refactor(connector): [WorldPay] migrate from modular to standard payment APIs (#6317)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Kashif
2024-10-21 15:29:44 +05:30
committed by GitHub
parent 0dba763c3a
commit 58296ffae6
12 changed files with 531 additions and 268 deletions

1
Cargo.lock generated
View File

@ -6438,6 +6438,7 @@ dependencies = [
"unicode-segmentation",
"unidecode",
"url",
"urlencoding",
"utoipa",
"uuid",
"validator",

View File

@ -3416,6 +3416,12 @@ key1="Password"
api_secret="Merchant Identifier"
[worldpay.connector_webhook_details]
merchant_secret="Source verification key"
[worldpay.metadata.merchant_name]
name="merchant_name"
label="Name of the merchant to de displayed during 3DS challenge"
placeholder="Enter Name of the merchant"
required=true
type="Text"
[[worldpay.metadata.apple_pay]]
name="certificate"

View File

@ -2473,6 +2473,12 @@ merchant_secret="Source verification key"
api_key="Username"
key1="Password"
api_secret="Merchant Identifier"
[worldpay.metadata.merchant_name]
name="merchant_name"
label="Name of the merchant to de displayed during 3DS challenge"
placeholder="Enter Name of the merchant"
required=true
type="Text"
[[worldpay.metadata.apple_pay]]
name="certificate"

View File

@ -3406,6 +3406,12 @@ key1="Password"
api_secret="Merchant Identifier"
[worldpay.connector_webhook_details]
merchant_secret="Source verification key"
[worldpay.metadata.merchant_name]
name="merchant_name"
label="Name of the merchant to de displayed during 3DS challenge"
placeholder="Enter Name of the merchant"
required=true
type="Text"
[[worldpay.metadata.apple_pay]]
name="certificate"

View File

@ -115,6 +115,7 @@ tracing-futures = { version = "0.2.5", features = ["tokio"] }
unicode-segmentation = "1.11.0"
unidecode = "0.3.0"
url = { version = "2.5.0", features = ["serde"] }
urlencoding = "2.1.3"
utoipa = { version = "4.2.0", features = ["preserve_order", "preserve_path_order", "time"] }
uuid = { version = "1.8.0", features = ["v4"] }
validator = "0.17.0"

View File

@ -725,6 +725,7 @@ impl PaymentsPreProcessingData for types::PaymentsPreProcessingData {
pub trait PaymentsCaptureRequestData {
fn is_multiple_capture(&self) -> bool;
fn get_browser_info(&self) -> Result<BrowserInformation, Error>;
fn get_capture_method(&self) -> Option<enums::CaptureMethod>;
}
impl PaymentsCaptureRequestData for types::PaymentsCaptureData {
@ -736,6 +737,9 @@ impl PaymentsCaptureRequestData for types::PaymentsCaptureData {
.clone()
.ok_or_else(missing_field_err("browser_info"))
}
fn get_capture_method(&self) -> Option<enums::CaptureMethod> {
self.capture_method.to_owned()
}
}
pub trait RevokeMandateRequestData {

View File

@ -16,6 +16,7 @@ use self::{requests::*, response::*};
use super::utils::{self as connector_utils, RefundsRequestData};
use crate::{
configs::settings,
consts,
core::errors::{self, CustomResult},
events::connector_api_logs::ConnectorEvent,
headers,
@ -64,6 +65,7 @@ where
headers::CONTENT_TYPE.to_string(),
self.get_content_type().to_string().into(),
),
(headers::X_WP_API_VERSION.to_string(), "2024-06-01".into()),
];
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
headers.append(&mut api_key);
@ -81,7 +83,7 @@ impl ConnectorCommon for Worldpay {
}
fn common_get_content_type(&self) -> &'static str {
"application/vnd.worldpay.payments-v7+json"
"application/json"
}
fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str {
@ -205,8 +207,9 @@ impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsR
) -> CustomResult<String, errors::ConnectorError> {
let connector_payment_id = req.request.connector_transaction_id.clone();
Ok(format!(
"{}payments/authorizations/cancellations/{connector_payment_id}",
"{}api/payments/{}/cancellations",
self.base_url(connectors),
urlencoding::encode(&connector_payment_id),
))
}
@ -244,15 +247,24 @@ impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsR
.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(consts::WP_CORRELATION_ID)
.and_then(|header_value| header_value.to_str().ok())
.map(|id| id.to_string())
});
Ok(types::PaymentsCancelRouterData {
status: enums::AttemptStatus::Voided,
status: enums::AttemptStatus::from(response.outcome.clone()),
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::foreign_try_from(response.links)?,
resource_id: types::ResponseId::foreign_try_from((
response,
Some(data.request.connector_transaction_id.clone()),
))?,
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: None,
connector_response_reference_id: optional_correlation_id,
incremental_authorization_allowed: None,
charge_id: None,
}),
@ -306,9 +318,9 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
.get_connector_transaction_id()
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?;
Ok(format!(
"{}payments/events/{}",
"{}api/payments/{}",
self.base_url(connectors),
connector_payment_id
urlencoding::encode(&connector_payment_id),
))
}
@ -349,6 +361,12 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
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(consts::WP_CORRELATION_ID)
.and_then(|header_value| header_value.to_str().ok())
.map(|id| id.to_string())
});
let attempt_status = data.status;
let worldpay_status = response.last_event;
let status = match (attempt_status, worldpay_status.clone()) {
@ -371,7 +389,7 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
mandate_reference: None,
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: None,
connector_response_reference_id: optional_correlation_id,
incremental_authorization_allowed: None,
charge_id: None,
}),
@ -403,9 +421,9 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
) -> CustomResult<String, errors::ConnectorError> {
let connector_payment_id = req.request.connector_transaction_id.clone();
Ok(format!(
"{}payments/settlements/partials/{}",
"{}api/payments/{}/partialSettlements",
self.base_url(connectors),
connector_payment_id
urlencoding::encode(&connector_payment_id),
))
}
@ -457,15 +475,24 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
.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(consts::WP_CORRELATION_ID)
.and_then(|header_value| header_value.to_str().ok())
.map(|id| id.to_string())
});
Ok(types::PaymentsCaptureRouterData {
status: enums::AttemptStatus::Pending,
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::foreign_try_from(response.links)?,
resource_id: types::ResponseId::foreign_try_from((
response,
Some(data.request.connector_transaction_id.clone()),
))?,
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: None,
connector_response_reference_id: optional_correlation_id,
incremental_authorization_allowed: None,
charge_id: None,
}),
@ -514,10 +541,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
_req: &types::PaymentsAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}cardPayments/customerInitiatedTransactions",
self.base_url(connectors)
))
Ok(format!("{}api/payments", self.base_url(connectors)))
}
fn get_request_body(
@ -573,12 +597,21 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
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(consts::WP_CORRELATION_ID)
.and_then(|header_value| header_value.to_str().ok())
.map(|id| id.to_string())
});
types::RouterData::try_from(types::ResponseRouterData {
types::RouterData::foreign_try_from((
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
},
optional_correlation_id,
))
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
@ -631,9 +664,9 @@ impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsRespon
) -> CustomResult<String, errors::ConnectorError> {
let connector_payment_id = req.request.connector_transaction_id.clone();
Ok(format!(
"{}payments/settlements/refunds/partials/{}",
"{}api/payments/{}/partialRefunds",
self.base_url(connectors),
connector_payment_id
urlencoding::encode(&connector_payment_id),
))
}
@ -670,9 +703,19 @@ impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsRespon
.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(consts::WP_CORRELATION_ID)
.and_then(|header_value| header_value.to_str().ok())
.map(|id| id.to_string())
});
Ok(types::RefundExecuteRouterData {
response: Ok(types::RefundsResponseData {
connector_refund_id: ResponseIdStr::try_from(response.links)?.id,
connector_refund_id: ResponseIdStr::foreign_try_from((
response,
optional_correlation_id,
))?
.id,
refund_status: enums::RefundStatus::Pending,
}),
..data.clone()
@ -710,9 +753,9 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}payments/events/{}",
"{}api/payments/{}",
self.base_url(connectors),
req.request.get_connector_refund_id()?
urlencoding::encode(&req.request.get_connector_refund_id()?),
))
}
@ -813,7 +856,7 @@ impl api::IncomingWebhook for Worldpay {
.parse_struct("WorldpayWebhookTransactionId")
.change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
api::PaymentIdType::ConnectorTransactionId(body.event_details.transaction_reference),
api::PaymentIdType::PaymentAttemptId(body.event_details.transaction_reference),
))
}
@ -829,13 +872,14 @@ impl api::IncomingWebhook for Worldpay {
EventType::Authorized => {
Ok(api::IncomingWebhookEvent::PaymentIntentAuthorizationSuccess)
}
EventType::SentForSettlement => Ok(api::IncomingWebhookEvent::PaymentIntentProcessing),
EventType::Settled => Ok(api::IncomingWebhookEvent::PaymentIntentSuccess),
EventType::SentForSettlement | EventType::SentForAuthorization => {
Ok(api::IncomingWebhookEvent::PaymentIntentProcessing)
}
EventType::Error | EventType::Expired | EventType::SettlementFailed => {
Ok(api::IncomingWebhookEvent::PaymentIntentFailure)
}
EventType::Unknown
| EventType::SentForAuthorization
| EventType::Cancelled
| EventType::Refused
| EventType::Refunded

View File

@ -1,5 +1,99 @@
use masking::Secret;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorldpayPaymentsRequest {
pub transaction_reference: String,
pub merchant: Merchant,
pub instruction: Instruction,
#[serde(skip_serializing_if = "Option::is_none")]
pub customer: Option<Customer>,
}
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Merchant {
pub entity: Secret<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mcc: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub payment_facilitator: Option<PaymentFacilitator>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Instruction {
pub settlement: Option<AutoSettlement>,
pub method: PaymentMethod,
pub payment_instrument: PaymentInstrument,
pub narrative: InstructionNarrative,
pub value: PaymentValue,
#[serde(skip_serializing_if = "Option::is_none")]
pub debt_repayment: Option<bool>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PaymentInstrument {
Card(CardPayment),
CardToken(CardToken),
Googlepay(WalletPayment),
Applepay(WalletPayment),
}
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CardPayment {
#[serde(rename = "type")]
pub payment_type: PaymentType,
#[serde(skip_serializing_if = "Option::is_none")]
pub card_holder_name: Option<Secret<String>>,
pub card_number: cards::CardNumber,
pub expiry_date: ExpiryDate,
#[serde(skip_serializing_if = "Option::is_none")]
pub billing_address: Option<BillingAddress>,
pub cvc: Secret<String>,
}
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CardToken {
#[serde(rename = "type")]
pub payment_type: PaymentType,
pub href: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cvc: Option<Secret<String>>,
}
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WalletPayment {
#[serde(rename = "type")]
pub payment_type: PaymentType,
pub wallet_token: Secret<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub billing_address: Option<BillingAddress>,
}
#[derive(
Clone, Copy, Debug, Eq, Default, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
)]
#[serde(rename_all = "lowercase")]
pub enum PaymentType {
#[default]
Plain,
Token,
Encrypted,
Checkout,
}
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
pub struct ExpiryDate {
pub month: Secret<i8>,
pub year: Secret<i32>,
}
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BillingAddress {
@ -17,17 +111,6 @@ pub struct BillingAddress {
pub country_code: common_enums::CountryAlpha2,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorldpayPaymentsRequest {
pub transaction_reference: String,
pub merchant: Merchant,
pub instruction: Instruction,
pub channel: Channel,
#[serde(skip_serializing_if = "Option::is_none")]
pub customer: Option<Customer>,
}
#[derive(
Clone, Copy, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
)]
@ -100,89 +183,23 @@ pub struct NetworkToken {
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Instruction {
pub request_auto_settlement: RequestAutoSettlement,
pub narrative: InstructionNarrative,
pub value: PaymentValue,
pub payment_instrument: PaymentInstrument,
#[serde(skip_serializing_if = "Option::is_none")]
pub debt_repayment: Option<bool>,
pub struct AutoSettlement {
pub auto: bool,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RequestAutoSettlement {
pub enabled: bool,
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PaymentMethod {
#[default]
Card,
ApplePay,
GooglePay,
}
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InstructionNarrative {
pub line1: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub line2: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PaymentInstrument {
Card(CardPayment),
CardToken(CardToken),
Googlepay(WalletPayment),
Applepay(WalletPayment),
}
#[derive(
Clone, Copy, Debug, Eq, Default, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
)]
pub enum PaymentType {
#[default]
#[serde(rename = "card/plain")]
Card,
#[serde(rename = "card/token")]
CardToken,
#[serde(rename = "card/wallet+googlepay")]
Googlepay,
#[serde(rename = "card/wallet+applepay")]
Applepay,
}
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CardPayment {
#[serde(rename = "type")]
pub payment_type: PaymentType,
pub card_number: cards::CardNumber,
pub expiry_date: ExpiryDate,
#[serde(skip_serializing_if = "Option::is_none")]
pub card_holder_name: Option<Secret<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub billing_address: Option<BillingAddress>,
pub cvc: Secret<String>,
}
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CardToken {
#[serde(rename = "type")]
pub payment_type: PaymentType,
pub href: String,
}
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WalletPayment {
#[serde(rename = "type")]
pub payment_type: PaymentType,
pub wallet_token: Secret<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub billing_address: Option<BillingAddress>,
}
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
pub struct ExpiryDate {
pub month: Secret<i8>,
pub year: Secret<i32>,
}
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
@ -191,16 +208,6 @@ pub struct PaymentValue {
pub currency: api_models::enums::Currency,
}
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Merchant {
pub entity: Secret<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mcc: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub payment_facilitator: Option<PaymentFacilitator>,
}
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PaymentFacilitator {

View File

@ -1,25 +1,97 @@
use error_stack::ResultExt;
use masking::Secret;
use serde::{Deserialize, Serialize};
use super::requests::*;
use crate::{core::errors, types, types::transformers::ForeignTryFrom};
use crate::core::errors;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorldpayPaymentsResponse {
pub outcome: Option<PaymentOutcome>,
/// Any risk factors which have been identified for the authorization. This section will not appear if no risks are identified.
pub outcome: PaymentOutcome,
pub transaction_reference: Option<String>,
#[serde(flatten)]
pub other_fields: WorldpayPaymentResponseFields,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum WorldpayPaymentResponseFields {
AuthorizedResponse(Box<AuthorizedResponse>),
DDCResponse(DDCResponse),
FraudHighRisk(FraudHighRiskResponse),
RefusedResponse(RefusedResponse),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthorizedResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub risk_factors: Option<Vec<RiskFactorsInner>>,
pub payment_instrument: Option<PaymentsResPaymentInstrument>,
#[serde(skip_serializing_if = "Option::is_none")]
pub issuer: Option<Issuer>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scheme: Option<PaymentsResponseScheme>,
#[serde(skip_serializing_if = "Option::is_none")]
pub payment_instrument: Option<PaymentsResPaymentInstrument>,
#[serde(rename = "_links", skip_serializing_if = "Option::is_none")]
pub links: Option<PaymentLinks>,
pub links: Option<SelfLink>,
#[serde(rename = "_actions")]
pub actions: Option<ActionLinks>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub risk_factors: Option<Vec<RiskFactorsInner>>,
pub fraud: Option<Fraud>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct FraudHighRiskResponse {
pub score: f32,
pub reason: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RefusedResponse {
pub refusal_description: String,
pub refusal_code: String,
pub risk_factors: Vec<RiskFactorsInner>,
pub fraud: Fraud,
#[serde(rename = "threeDS")]
pub three_ds: Option<ThreeDsResponse>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreeDsResponse {
pub outcome: String,
pub issuer_response: IssuerResponse,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum IssuerResponse {
Challenged,
Frictionless,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DDCResponse {
pub device_data_collection: DDCToken,
#[serde(rename = "_actions")]
pub actions: DDCActionLink,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct DDCToken {
pub jwt: String,
pub url: String,
pub bin: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct DDCActionLink {
#[serde(rename = "supply3dsDeviceData")]
supply_ddc_data: ActionLink,
method: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -28,16 +100,57 @@ pub enum PaymentOutcome {
#[serde(alias = "authorized", alias = "Authorized")]
Authorized,
Refused,
#[serde(alias = "Sent for Settlement")]
SentForSettlement,
#[serde(alias = "Sent for Refund")]
SentForRefund,
FraudHighRisk,
#[serde(alias = "3dsDeviceDataRequired")]
ThreeDsDeviceDataRequired,
ThreeDsChallenged,
SentForCancellation,
#[serde(alias = "3dsAuthenticationFailed")]
ThreeDsAuthenticationFailed,
SentForPartialRefund,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum RefundOutcome {
#[serde(alias = "Sent for Refund")]
SentForRefund,
pub struct SelfLink {
#[serde(rename = "self")]
pub self_link: SelfLinkInner,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SelfLinkInner {
pub href: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ActionLinks {
supply_3ds_device_data: Option<ActionLink>,
settle_payment: Option<ActionLink>,
partially_settle_payment: Option<ActionLink>,
refund_payment: Option<ActionLink>,
partiall_refund_payment: Option<ActionLink>,
cancel_payment: Option<ActionLink>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ActionLink {
pub href: String,
pub method: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Fraud {
pub outcome: FraudOutcome,
pub score: f32,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum FraudOutcome {
LowRisk,
HighRisk,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -70,40 +183,6 @@ pub enum EventType {
Unknown,
}
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
pub struct PaymentLinks {
#[serde(
rename = "cardPayments:events",
skip_serializing_if = "Option::is_none"
)]
pub events: Option<PaymentLink>,
#[serde(
rename = "cardPayments:settle",
skip_serializing_if = "Option::is_none"
)]
pub settle_event: Option<PaymentLink>,
#[serde(
rename = "cardPayments:partialSettle",
skip_serializing_if = "Option::is_none"
)]
pub partial_settle_event: Option<PaymentLink>,
#[serde(
rename = "cardPayments:refund",
skip_serializing_if = "Option::is_none"
)]
pub refund_event: Option<PaymentLink>,
#[serde(
rename = "cardPayments:partialRefund",
skip_serializing_if = "Option::is_none"
)]
pub partial_refund_event: Option<PaymentLink>,
#[serde(
rename = "cardPayments:reverse",
skip_serializing_if = "Option::is_none"
)]
pub reverse_event: Option<PaymentLink>,
}
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
pub struct EventLinks {
#[serde(rename = "payments:events", skip_serializing_if = "Option::is_none")]
@ -115,20 +194,42 @@ pub struct PaymentLink {
pub href: String,
}
fn get_resource_id<T, F>(
links: Option<PaymentLinks>,
pub fn get_resource_id<T, F>(
response: WorldpayPaymentsResponse,
connector_transaction_id: Option<String>,
transform_fn: F,
) -> Result<T, error_stack::Report<errors::ConnectorError>>
where
F: Fn(String) -> T,
{
let reference_id = links
.and_then(|l| l.events)
.and_then(|e| e.href.rsplit_once('/').map(|h| h.1.to_string()))
.map(transform_fn);
reference_id.ok_or_else(|| {
let reference_id = match response.other_fields {
WorldpayPaymentResponseFields::AuthorizedResponse(res) => res
.links
.as_ref()
.and_then(|link| link.self_link.href.rsplit_once('/'))
.map(|(_, h)| urlencoding::decode(h))
.transpose()
.change_context(errors::ConnectorError::ResponseHandlingFailed)?
.map(|s| transform_fn(s.into_owned())),
WorldpayPaymentResponseFields::DDCResponse(res) => res
.actions
.supply_ddc_data
.href
.split('/')
.rev()
.nth(1)
.map(urlencoding::decode)
.transpose()
.change_context(errors::ConnectorError::ResponseHandlingFailed)?
.map(|s| transform_fn(s.into_owned())),
WorldpayPaymentResponseFields::FraudHighRisk(_) => None,
WorldpayPaymentResponseFields::RefusedResponse(_) => None,
};
reference_id
.or_else(|| connector_transaction_id.map(transform_fn))
.ok_or_else(|| {
errors::ConnectorError::MissingRequiredField {
field_name: "links.events",
field_name: "_links.self.href",
}
.into()
})
@ -138,20 +239,6 @@ pub struct ResponseIdStr {
pub id: String,
}
impl TryFrom<Option<PaymentLinks>> for ResponseIdStr {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(links: Option<PaymentLinks>) -> Result<Self, Self::Error> {
get_resource_id(links, |id| Self { id })
}
}
impl ForeignTryFrom<Option<PaymentLinks>> for types::ResponseId {
type Error = error_stack::Report<errors::ConnectorError>;
fn foreign_try_from(links: Option<PaymentLinks>) -> Result<Self, Self::Error> {
get_resource_id(links, Self::ConnectorTransactionId)
}
}
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Issuer {
@ -173,10 +260,10 @@ pub struct PaymentsResPaymentInstrument {
pub payment_instrument_type: Option<String>,
pub card_bin: Option<String>,
pub last_four: Option<String>,
pub category: Option<String>,
pub expiry_date: Option<ExpiryDate>,
pub card_brand: Option<String>,
pub funding_type: Option<String>,
pub category: Option<String>,
pub issuer_name: Option<String>,
pub payment_account_reference: Option<String>,
}
@ -231,7 +318,7 @@ pub enum RiskType {
#[derive(
Clone, Copy, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
)]
#[serde(rename_all = "camelCase")]
#[serde(rename_all = "lowercase")]
pub enum Detail {
#[default]
Address,

View File

@ -1,11 +1,11 @@
use api_models::payments::Address;
use base64::Engine;
use common_utils::{errors::CustomResult, ext_traits::OptionExt, types::MinorUnit};
use common_utils::{errors::CustomResult, ext_traits::OptionExt, pii, types::MinorUnit};
use diesel_models::enums;
use error_stack::ResultExt;
use hyperswitch_connectors::utils::RouterData;
use masking::{PeekInterface, Secret};
use serde::Serialize;
use masking::{ExposeInterface, PeekInterface, Secret};
use serde::{Deserialize, Serialize};
use super::{requests::*, response::*};
use crate::{
@ -45,13 +45,38 @@ impl<T>
})
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct WorldpayConnectorMetadataObject {
pub merchant_name: Option<Secret<String>>,
}
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())
.change_context(errors::ConnectorError::InvalidConnectorConfig {
config: "metadata",
})?;
Ok(metadata)
}
}
fn fetch_payment_instrument(
payment_method: domain::PaymentMethodData,
billing_address: Option<&Address>,
auth_type: enums::AuthenticationType,
) -> CustomResult<PaymentInstrument, errors::ConnectorError> {
match payment_method {
domain::PaymentMethodData::Card(card) => Ok(PaymentInstrument::Card(CardPayment {
payment_type: PaymentType::Card,
domain::PaymentMethodData::Card(card) => {
if auth_type == enums::AuthenticationType::ThreeDs {
return Err(errors::ConnectorError::NotImplemented(
"ThreeDS flow through worldpay".to_string(),
)
.into());
}
Ok(PaymentInstrument::Card(CardPayment {
payment_type: PaymentType::Plain,
expiry_date: ExpiryDate {
month: utils::CardData::get_expiry_month_as_i8(&card)?,
year: utils::CardData::get_expiry_year_as_i32(&card)?,
@ -81,17 +106,18 @@ fn fetch_payment_instrument(
} else {
None
},
})),
}))
}
domain::PaymentMethodData::Wallet(wallet) => match wallet {
domain::WalletData::GooglePay(data) => {
Ok(PaymentInstrument::Googlepay(WalletPayment {
payment_type: PaymentType::Googlepay,
payment_type: PaymentType::Encrypted,
wallet_token: Secret::new(data.tokenization_data.token),
..WalletPayment::default()
}))
}
domain::WalletData::ApplePay(data) => Ok(PaymentInstrument::Applepay(WalletPayment {
payment_type: PaymentType::Applepay,
payment_type: PaymentType::Encrypted,
wallet_token: Secret::new(data.payment_data),
..WalletPayment::default()
})),
@ -149,6 +175,27 @@ fn fetch_payment_instrument(
}
}
impl TryFrom<(enums::PaymentMethod, enums::PaymentMethodType)> for PaymentMethod {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
src: (enums::PaymentMethod, enums::PaymentMethodType),
) -> Result<Self, Self::Error> {
match (src.0, src.1) {
(enums::PaymentMethod::Card, _) => Ok(Self::Card),
(enums::PaymentMethod::Wallet, enums::PaymentMethodType::ApplePay) => {
Ok(Self::ApplePay)
}
(enums::PaymentMethod::Wallet, enums::PaymentMethodType::GooglePay) => {
Ok(Self::GooglePay)
}
_ => Err(errors::ConnectorError::NotImplemented(
utils::get_unimplemented_payment_method_error_message("worldpay"),
)
.into()),
}
}
}
impl
TryFrom<(
&WorldpayRouterData<
@ -176,28 +223,44 @@ impl
),
) -> Result<Self, Self::Error> {
let (item, entity_id) = req;
let worldpay_connector_metadata_object: WorldpayConnectorMetadataObject =
WorldpayConnectorMetadataObject::try_from(&item.router_data.connector_meta_data)?;
let merchant_name = worldpay_connector_metadata_object.merchant_name.ok_or(
errors::ConnectorError::InvalidConnectorConfig {
config: "metadata.merchant_name",
},
)?;
Ok(Self {
instruction: Instruction {
request_auto_settlement: RequestAutoSettlement {
enabled: item.router_data.request.capture_method
== Some(enums::CaptureMethod::Automatic),
settlement: item
.router_data
.request
.capture_method
.map(|capture_method| AutoSettlement {
auto: capture_method == enums::CaptureMethod::Automatic,
}),
method: item
.router_data
.request
.payment_method_type
.map(|pmt| PaymentMethod::try_from((item.router_data.payment_method, pmt)))
.transpose()?
.get_required_value("payment_method")
.change_context(errors::ConnectorError::MissingRequiredField {
field_name: "payment_method",
})?,
payment_instrument: fetch_payment_instrument(
item.router_data.request.payment_method_data.clone(),
item.router_data.get_optional_billing(),
item.router_data.auth_type,
)?,
narrative: InstructionNarrative {
line1: merchant_name.expose(),
},
value: PaymentValue {
amount: item.amount,
currency: item.router_data.request.currency,
},
narrative: InstructionNarrative {
line1: item
.router_data
.merchant_id
.get_string_repr()
.replace('_', "-"),
..Default::default()
},
payment_instrument: fetch_payment_instrument(
item.router_data.request.payment_method_data.clone(),
item.router_data.get_optional_billing(),
)?,
debt_repayment: None,
},
merchant: Merchant {
@ -205,7 +268,6 @@ impl
..Default::default()
},
transaction_reference: item.router_data.connector_request_reference_id.clone(),
channel: Channel::Ecom,
customer: None,
})
}
@ -250,9 +312,15 @@ impl From<PaymentOutcome> for enums::AttemptStatus {
fn from(item: PaymentOutcome) -> Self {
match item {
PaymentOutcome::Authorized => Self::Authorized,
PaymentOutcome::Refused => Self::Failure,
PaymentOutcome::SentForSettlement => Self::CaptureInitiated,
PaymentOutcome::SentForRefund => Self::AutoRefunded,
PaymentOutcome::ThreeDsDeviceDataRequired => Self::DeviceDataCollectionPending,
PaymentOutcome::ThreeDsAuthenticationFailed => Self::AuthenticationFailed,
PaymentOutcome::ThreeDsChallenged => Self::AuthenticationPending,
PaymentOutcome::SentForCancellation => Self::VoidInitiated,
PaymentOutcome::SentForPartialRefund | PaymentOutcome::SentForRefund => {
Self::AutoRefunded
}
PaymentOutcome::Refused | PaymentOutcome::FraudHighRisk => Self::Failure,
}
}
}
@ -295,32 +363,43 @@ impl From<EventType> for enums::RefundStatus {
}
}
impl TryFrom<types::PaymentsResponseRouterData<WorldpayPaymentsResponse>>
for types::PaymentsAuthorizeRouterData
impl
ForeignTryFrom<(
types::PaymentsResponseRouterData<WorldpayPaymentsResponse>,
Option<String>,
)> for types::PaymentsAuthorizeRouterData
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::PaymentsResponseRouterData<WorldpayPaymentsResponse>,
fn foreign_try_from(
item: (
types::PaymentsResponseRouterData<WorldpayPaymentsResponse>,
Option<String>,
),
) -> Result<Self, Self::Error> {
let (router_data, optional_correlation_id) = item;
let description = match router_data.response.other_fields {
WorldpayPaymentResponseFields::AuthorizedResponse(ref res) => res.description.clone(),
WorldpayPaymentResponseFields::DDCResponse(_)
| WorldpayPaymentResponseFields::FraudHighRisk(_)
| WorldpayPaymentResponseFields::RefusedResponse(_) => None,
};
Ok(Self {
status: match item.response.outcome {
Some(outcome) => enums::AttemptStatus::from(outcome),
None => Err(errors::ConnectorError::MissingRequiredField {
field_name: "outcome",
})?,
},
description: item.response.description,
status: enums::AttemptStatus::from(router_data.response.outcome.clone()),
description,
response: Ok(PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::foreign_try_from(item.response.links)?,
resource_id: types::ResponseId::foreign_try_from((
router_data.response,
optional_correlation_id.clone(),
))?,
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: None,
connector_response_reference_id: optional_correlation_id,
incremental_authorization_allowed: None,
charge_id: None,
}),
..item.data
..router_data.data
})
}
}
@ -362,3 +441,21 @@ impl TryFrom<WorldpayWebhookEventType> for WorldpayEventResponse {
})
}
}
impl ForeignTryFrom<(WorldpayPaymentsResponse, Option<String>)> for ResponseIdStr {
type Error = error_stack::Report<errors::ConnectorError>;
fn foreign_try_from(
item: (WorldpayPaymentsResponse, Option<String>),
) -> Result<Self, Self::Error> {
get_resource_id(item.0, item.1, |id| Self { id })
}
}
impl ForeignTryFrom<(WorldpayPaymentsResponse, Option<String>)> for types::ResponseId {
type Error = error_stack::Report<errors::ConnectorError>;
fn foreign_try_from(
item: (WorldpayPaymentsResponse, Option<String>),
) -> Result<Self, Self::Error> {
get_resource_id(item.0, item.1, Self::ConnectorTransactionId)
}
}

View File

@ -177,3 +177,6 @@ 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

@ -87,6 +87,7 @@ pub mod headers {
pub const X_APP_ID: &str = "x-app-id";
pub const X_REDIRECT_URI: &str = "x-redirect-uri";
pub const X_TENANT_ID: &str = "x-tenant-id";
pub const X_WP_API_VERSION: &str = "WP-Api-Version";
}
pub mod pii {