feat(connector): [WorldpayWPG] Implement cards and apple pay decrypt payout (#10545)

Co-authored-by: Vani Gupta <vani.gupta@juspay.in>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Vani Gupta
2025-12-05 19:34:37 +05:30
committed by GitHub
parent 2c9e6307ff
commit 8ddc861071
3 changed files with 350 additions and 108 deletions

View File

@@ -34,7 +34,10 @@ use hyperswitch_domain_models::{
};
#[cfg(feature = "payouts")]
use hyperswitch_domain_models::{
router_flow_types::payouts::{PoCancel, PoFulfill},
router_flow_types::{
payouts::{PoCancel, PoFulfill},
PoSync,
},
router_request_types::PayoutsData,
router_response_types::PayoutsResponseData,
types::PayoutsRouterData,
@@ -53,7 +56,11 @@ use hyperswitch_interfaces::{
use masking::{ExposeInterface, Mask};
use transformers as worldpayxml;
use crate::{constants::headers, types::ResponseRouterData, utils};
use crate::{
constants::headers,
types::ResponseRouterData,
utils::{self},
};
#[derive(Clone)]
pub struct Worldpayxml {
@@ -772,6 +779,8 @@ impl api::Payouts for Worldpayxml {}
impl api::PayoutFulfill for Worldpayxml {}
#[cfg(feature = "payouts")]
impl api::PayoutCancel for Worldpayxml {}
#[cfg(feature = "payouts")]
impl api::PayoutSync for Worldpayxml {}
#[async_trait::async_trait]
#[cfg(feature = "payouts")]
@@ -875,6 +884,97 @@ impl ConnectorIntegration<PoFulfill, PayoutsData, PayoutsResponseData> for World
}
}
#[async_trait::async_trait]
#[cfg(feature = "payouts")]
impl ConnectorIntegration<PoSync, PayoutsData, PayoutsResponseData> for Worldpayxml {
fn get_url(
&self,
_req: &PayoutsRouterData<PoSync>,
connectors: &Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(self.base_url(connectors).to_owned())
}
fn get_headers(
&self,
req: &PayoutsRouterData<PoSync>,
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_request_body(
&self,
req: &PayoutsRouterData<PoSync>,
_connectors: &Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let connector_req_object = worldpayxml::PaymentService::try_from(req)?;
let connector_req = utils::XmlSerializer::serialize_to_xml_bytes(
&connector_req_object,
worldpayxml::worldpayxml_constants::XML_VERSION,
Some(worldpayxml::worldpayxml_constants::XML_ENCODING),
None,
Some(worldpayxml::worldpayxml_constants::WORLDPAYXML_DOC_TYPE),
)?;
Ok(RequestContent::RawBytes(connector_req))
}
fn build_request(
&self,
req: &PayoutsRouterData<PoSync>,
connectors: &Connectors,
) -> CustomResult<Option<Request>, errors::ConnectorError> {
let request = RequestBuilder::new()
.method(Method::Post)
.url(&types::PayoutSyncType::get_url(self, req, connectors)?)
.attach_default_headers()
.headers(types::PayoutSyncType::get_headers(self, req, connectors)?)
.set_body(types::PayoutSyncType::get_request_body(
self, req, connectors,
)?)
.build();
Ok(Some(request))
}
fn handle_response(
&self,
data: &PayoutsRouterData<PoSync>,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<PayoutsRouterData<PoSync>, errors::ConnectorError> {
let response: worldpayxml::PaymentService =
utils::deserialize_xml_to_struct(&res.response)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
RouterData::try_from(ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
.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)
}
}
#[async_trait::async_trait]
#[cfg(feature = "payouts")]
impl ConnectorIntegration<PoCancel, PayoutsData, PayoutsResponseData> for Worldpayxml {

View File

@@ -10,7 +10,7 @@ use error_stack::ResultExt;
#[cfg(feature = "payouts")]
use hyperswitch_domain_models::{
address::Address,
router_flow_types::payouts::{PoCancel, PoFulfill},
router_flow_types::payouts::{PoCancel, PoFulfill, PoSync},
router_response_types::PayoutsResponseData,
types::PayoutsRouterData,
};
@@ -144,21 +144,22 @@ pub struct Reply {
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PayoutResponse {
pub reply: PayoutReply,
reply: PayoutReply,
}
#[cfg(feature = "payouts")]
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PayoutReply {
pub ok: Option<OkPayoutResponse>,
pub error: Option<WorldpayXmlErrorResponse>,
struct PayoutReply {
ok: Option<OkPayoutResponse>,
order_status: Option<OrderStatus>,
error: Option<WorldpayXmlErrorResponse>,
}
#[cfg(feature = "payouts")]
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OkPayoutResponse {
struct OkPayoutResponse {
refund_received: Option<ModifyRequestReceived>,
cancel_received: Option<ModifyRequestReceived>,
}
@@ -285,6 +286,10 @@ pub enum LastEvent {
RefundRequested,
RefundFailed,
RefundedByMerchant,
Error,
QueryRequired,
CancelReceived,
RefundReceived,
}
#[derive(Debug, Deserialize, Serialize)]
@@ -474,24 +479,18 @@ struct GooglePayData {
}
#[cfg(feature = "payouts")]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PayoutOutcome {
RefundReceived,
Refused,
Error,
QueryRequired,
CancelReceived,
}
#[cfg(feature = "payouts")]
impl From<PayoutOutcome> for enums::PayoutStatus {
fn from(item: PayoutOutcome) -> Self {
impl TryFrom<LastEvent> for enums::PayoutStatus {
type Error = errors::ConnectorError;
fn try_from(item: LastEvent) -> Result<Self, Self::Error> {
match item {
PayoutOutcome::RefundReceived => Self::Initiated,
PayoutOutcome::Error | PayoutOutcome::Refused => Self::Failed,
PayoutOutcome::QueryRequired => Self::Pending,
PayoutOutcome::CancelReceived => Self::Cancelled,
LastEvent::SentForRefund | LastEvent::RefundReceived => Ok(Self::Initiated),
LastEvent::Error | LastEvent::Refused => Ok(Self::Failed),
LastEvent::QueryRequired => Ok(Self::Pending),
LastEvent::CancelReceived => Ok(Self::Cancelled),
LastEvent::RefundedByMerchant => Ok(Self::Success),
_ => Err(errors::ConnectorError::UnexpectedResponseError(
bytes::Bytes::from("Invalid LastEvent".to_string()),
)),
}
}
}
@@ -961,12 +960,7 @@ fn get_attempt_status(
LastEvent::Cancelled => Ok(common_enums::AttemptStatus::Voided),
LastEvent::Captured | LastEvent::Settled => Ok(common_enums::AttemptStatus::Charged),
LastEvent::SentForAuthorisation => Ok(common_enums::AttemptStatus::Authorizing),
LastEvent::Refunded
| LastEvent::SentForRefund
| LastEvent::RefundRequested
| LastEvent::SentForFastRefund
| LastEvent::RefundedByMerchant
| LastEvent::RefundFailed => Err(errors::ConnectorError::UnexpectedResponseError(
_ => Err(errors::ConnectorError::UnexpectedResponseError(
bytes::Bytes::from("Invalid LastEvent".to_string()),
)),
}
@@ -981,10 +975,7 @@ fn get_refund_status(last_event: LastEvent) -> Result<enums::RefundStatus, error
| LastEvent::RefundedByMerchant => Ok(enums::RefundStatus::Pending),
LastEvent::RefundFailed => Ok(enums::RefundStatus::Failure),
LastEvent::Captured | LastEvent::Settled => Ok(enums::RefundStatus::Pending),
LastEvent::Authorised
| LastEvent::Refused
| LastEvent::Cancelled
| LastEvent::SentForAuthorisation => Err(errors::ConnectorError::UnexpectedResponseError(
_ => Err(errors::ConnectorError::UnexpectedResponseError(
bytes::Bytes::from("Invalid LastEvent".to_string()),
)),
}
@@ -1495,7 +1486,7 @@ impl TryFrom<&RefundSyncRouterData> for PaymentService {
#[cfg(feature = "payouts")]
impl TryFrom<(ApplePayDecrypt, Option<CardAddress>, Option<String>)> for PaymentDetails {
type Error = error_stack::Report<errors::ConnectorError>;
type Error = errors::ConnectorError;
fn try_from(
(apple_pay_decrypted_data, address, purpose_of_payment): (
ApplePayDecrypt,
@@ -1514,22 +1505,22 @@ impl TryFrom<(ApplePayDecrypt, Option<CardAddress>, Option<String>)> for Payment
card_holder_name: apple_pay_decrypted_data.card_holder_name.clone(),
cvc: None,
card_address: address,
purpose_of_payment_code: None,
purpose_of_payment_code: purpose_of_payment,
};
let payment_method = match apple_pay_decrypted_data.card_network {
Some(CardNetwork::Visa) => PaymentMethod::VisaSSL(CardSSL {
purpose_of_payment_code: purpose_of_payment.clone(),
..card_data
}),
Some(CardNetwork::Mastercard) => PaymentMethod::EcmcSSL(CardSSL {
purpose_of_payment_code: purpose_of_payment.clone(),
..card_data
}),
_ => PaymentMethod::CardSSL(CardSSL {
purpose_of_payment_code: None,
..card_data
}),
let card_network = apple_pay_decrypted_data.card_network.ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "card_network",
},
)?;
let payment_method = match card_network {
CardNetwork::Visa => PaymentMethod::VisaSSL(CardSSL { ..card_data }),
CardNetwork::Mastercard => PaymentMethod::EcmcSSL(CardSSL { ..card_data }),
_ => Err(errors::ConnectorError::NotSupported {
message: format!("{} card network is not supported", card_network).to_string(),
connector: "WorldpayWPG Payout",
})?,
};
Ok(Self {
@@ -1560,22 +1551,23 @@ impl TryFrom<(CardPayout, Option<CardAddress>, Option<String>)> for PaymentDetai
card_holder_name: card_payout.card_holder_name.to_owned(),
cvc: None,
card_address: address,
purpose_of_payment_code: None,
purpose_of_payment_code: purpose_of_payment,
};
let payment_method = match card_payout.card_network {
Some(CardNetwork::Visa) => PaymentMethod::VisaSSL(CardSSL {
purpose_of_payment_code: purpose_of_payment.clone(),
..card_data
}),
Some(CardNetwork::Mastercard) => PaymentMethod::EcmcSSL(CardSSL {
purpose_of_payment_code: purpose_of_payment.clone(),
..card_data
}),
_ => PaymentMethod::CardSSL(CardSSL {
purpose_of_payment_code: None,
..card_data
}),
let card_network =
card_payout
.card_network
.ok_or(errors::ConnectorError::MissingRequiredField {
field_name: "card_network",
})?;
let payment_method = match card_network {
CardNetwork::Visa => PaymentMethod::VisaSSL(CardSSL { ..card_data }),
CardNetwork::Mastercard => PaymentMethod::EcmcSSL(CardSSL { ..card_data }),
_ => Err(errors::ConnectorError::NotSupported {
message: format!("{} card network is not supported", card_network).to_string(),
connector: "WorldpayWPG Payout",
})?,
};
Ok(Self {
@@ -1713,35 +1705,184 @@ impl TryFrom<PayoutsResponseRouterData<PoFulfill, PayoutResponse>>
) -> Result<Self, Self::Error> {
let reply = item.response.reply;
match (reply.error, reply.ok) {
(Some(error), None) => Ok(Self {
match (reply.error, reply.order_status, reply.ok) {
(Some(error), None, None) => Ok(Self {
status: common_enums::AttemptStatus::Failure,
response: Ok(PayoutsResponseData {
status: Some(enums::PayoutStatus::from(PayoutOutcome::Error)),
connector_payout_id: None,
payout_eligible: None,
should_add_next_step_to_process_tracker: false,
error_code: Some(error.code),
error_message: Some(error.message),
payout_connector_metadata: None,
}),
..item.data
}),
(None, Some(ok_status)) => Ok(Self {
response: Ok(PayoutsResponseData {
status: Some(enums::PayoutStatus::from(PayoutOutcome::RefundReceived)),
connector_payout_id: ok_status.refund_received.map(|id| id.order_code),
payout_eligible: None,
should_add_next_step_to_process_tracker: false,
error_code: None,
error_message: None,
payout_connector_metadata: None,
response: Err(ErrorResponse {
code: error.code,
message: error.message.clone(),
reason: Some(error.message.clone()),
status_code: item.http_code,
attempt_status: None,
connector_transaction_id: None,
network_advice_code: None,
network_decline_code: None,
network_error_message: None,
connector_metadata: None,
}),
..item.data
}),
(None, Some(order_status), None) => {
match (order_status.payment, order_status.error) {
(Some(payment), None) => Ok(Self {
response: Ok(PayoutsResponseData {
status: Some(enums::PayoutStatus::try_from(payment.last_event)?),
connector_payout_id: Some(order_status.order_code),
payout_eligible: None,
should_add_next_step_to_process_tracker: false,
error_code: None,
error_message: None,
payout_connector_metadata: None,
}),
..item.data
}),
(None, Some(error)) => Ok(Self {
status: common_enums::AttemptStatus::Failure,
response: Ok(PayoutsResponseData {
status: Some(enums::PayoutStatus::try_from(LastEvent::Error)?),
connector_payout_id: Some(order_status.order_code),
payout_eligible: None,
should_add_next_step_to_process_tracker: false,
error_code: Some(error.code),
error_message: Some(error.message),
payout_connector_metadata: None,
}),
..item.data
}),
_ => Err(
errors::ConnectorError::UnexpectedResponseError(bytes::Bytes::from(
"Either order_status.error or order_status.payment must be present in the response",
))
.into(),
),
}
},
(None, None, Some(ok_response)) => {
let response = ok_response.refund_received.ok_or(
errors::ConnectorError::UnexpectedResponseError(bytes::Bytes::from(
"ok.refund_received must be present in the response",
)),
)?;
Ok(Self {
response: Ok(PayoutsResponseData {
status: Some(enums::PayoutStatus::try_from(LastEvent::RefundReceived)?),
connector_payout_id: Some(response.order_code),
payout_eligible: None,
should_add_next_step_to_process_tracker: false,
error_code: None,
error_message: None,
payout_connector_metadata: None,
}),
..item.data
})
}
_ => Err(
errors::ConnectorError::UnexpectedResponseError(bytes::Bytes::from(
"Either reply.error or reply.ok must be present in the response",
"Either reply.error or reply.order_status must be present in the response",
))
.into(),
),
}
}
}
#[cfg(feature = "payouts")]
impl TryFrom<&PayoutsRouterData<PoSync>> for PaymentService {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &PayoutsRouterData<PoSync>) -> Result<Self, Self::Error> {
let order_code = item.request.connector_payout_id.to_owned().ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "order_code",
},
)?;
let auth = WorldpayxmlAuthType::try_from(&item.connector_auth_type)
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
let inquiry = Some(Inquiry {
order_inquiry: OrderInquiry { order_code },
});
Ok(Self {
version: worldpayxml_constants::WORLDPAYXML_VERSION.to_string(),
merchant_code: auth.merchant_code.clone(),
submit: None,
reply: None,
inquiry,
modify: None,
})
}
}
#[cfg(feature = "payouts")]
impl TryFrom<PayoutsResponseRouterData<PoSync, PaymentService>> for PayoutsRouterData<PoSync> {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: PayoutsResponseRouterData<PoSync, PaymentService>,
) -> Result<Self, Self::Error> {
let reply = item
.response
.reply
.ok_or(errors::ConnectorError::UnexpectedResponseError(
bytes::Bytes::from("Missing reply data".to_string()),
))?;
match (reply.error, reply.order_status) {
(Some(error), None) => Ok(Self {
status: common_enums::AttemptStatus::Failure,
response: Err(ErrorResponse {
code: error.code,
message: error.message.clone(),
reason: Some(error.message.clone()),
status_code: item.http_code,
attempt_status: None,
connector_transaction_id: None,
network_advice_code: None,
network_decline_code: None,
network_error_message: None,
connector_metadata: None,
}),
..item.data
}),
(None, Some(order_status)) => {
match (order_status.payment, order_status.error) {
(Some(payment), None) => Ok(Self {
response: Ok(PayoutsResponseData {
status: Some(enums::PayoutStatus::try_from(payment.last_event)?),
connector_payout_id: Some(order_status.order_code),
payout_eligible: None,
should_add_next_step_to_process_tracker: false,
error_code: None,
error_message: None,
payout_connector_metadata: None,
}),
..item.data
}),
(None, Some(_error)) => Ok(Self {
status: item.data.status,
response: Ok(PayoutsResponseData {
status: None,
connector_payout_id: Some(order_status.order_code),
payout_eligible: None,
should_add_next_step_to_process_tracker: false,
error_code: None,
error_message: None,
payout_connector_metadata: None,
}),
..item.data
}),
_ => Err(
errors::ConnectorError::UnexpectedResponseError(bytes::Bytes::from(
"Either order_status.error or order_status.payment must be present in the response",
))
.into(),
),
}
},
_ => Err(
errors::ConnectorError::UnexpectedResponseError(bytes::Bytes::from(
"Either reply.error or reply.order_status must be present in the response",
))
.into(),
),
@@ -1793,7 +1934,7 @@ impl TryFrom<PayoutsResponseRouterData<PoCancel, PayoutResponse>> for PayoutsRou
(Some(error), None) => Ok(Self {
status: common_enums::AttemptStatus::Failure,
response: Ok(PayoutsResponseData {
status: Some(enums::PayoutStatus::from(PayoutOutcome::Error)),
status: Some(enums::PayoutStatus::try_from(LastEvent::Error)?),
connector_payout_id: None,
payout_eligible: None,
should_add_next_step_to_process_tracker: false,
@@ -1803,18 +1944,26 @@ impl TryFrom<PayoutsResponseRouterData<PoCancel, PayoutResponse>> for PayoutsRou
}),
..item.data
}),
(None, Some(ok_status)) => Ok(Self {
response: Ok(PayoutsResponseData {
status: Some(enums::PayoutStatus::from(PayoutOutcome::CancelReceived)),
connector_payout_id: ok_status.refund_received.map(|id| id.order_code),
payout_eligible: None,
should_add_next_step_to_process_tracker: false,
error_code: None,
error_message: None,
payout_connector_metadata: None,
}),
..item.data
}),
(None, Some(ok_status)) => {
let response = ok_status.cancel_received.ok_or(
errors::ConnectorError::UnexpectedResponseError(bytes::Bytes::from(
"ok.cancel_received must be present in the response",
)),
)?;
Ok(Self {
response: Ok(PayoutsResponseData {
status: Some(enums::PayoutStatus::try_from(LastEvent::CancelReceived)?),
connector_payout_id: Some(response.order_code),
payout_eligible: None,
should_add_next_step_to_process_tracker: false,
error_code: None,
error_message: None,
payout_connector_metadata: None,
}),
..item.data
})
}
_ => Err(
errors::ConnectorError::UnexpectedResponseError(bytes::Bytes::from(
"Either reply.error or reply.ok must be present in the response",
@@ -1985,12 +2134,7 @@ pub fn get_payout_webhook_event(status: LastEvent) -> api_models::webhooks::Inco
LastEvent::Refused | LastEvent::RefundFailed => {
api_models::webhooks::IncomingWebhookEvent::PayoutFailure
}
LastEvent::Authorised
| LastEvent::Settled
| LastEvent::Captured
| LastEvent::SentForAuthorisation => {
api_models::webhooks::IncomingWebhookEvent::EventNotSupported
}
_ => api_models::webhooks::IncomingWebhookEvent::EventNotSupported,
}
}

View File

@@ -4447,7 +4447,6 @@ default_imp_for_payouts_retrieve!(
connectors::Worldline,
connectors::Worldpay,
connectors::Worldpayvantiv,
connectors::Worldpayxml,
connectors::Wellsfargo,
connectors::Wellsfargopayout,
connectors::Volt,
@@ -4889,7 +4888,6 @@ default_imp_for_payouts_cancel!(
connectors::Worldline,
connectors::Worldpay,
connectors::Worldpayvantiv,
// connectors::Worldpayxml,
connectors::Wellsfargo,
connectors::Wellsfargopayout,
connectors::Volt,