diff --git a/api-reference/v1/openapi_spec_v1.json b/api-reference/v1/openapi_spec_v1.json index c48b122e0d..00b5183c28 100644 --- a/api-reference/v1/openapi_spec_v1.json +++ b/api-reference/v1/openapi_spec_v1.json @@ -7776,6 +7776,61 @@ } } }, + "ApplePayDecrypt": { + "type": "object", + "required": [ + "dpan", + "expiry_month", + "expiry_year", + "card_holder_name" + ], + "properties": { + "dpan": { + "type": "string", + "description": "The dpan number associated with card number", + "example": "4242424242424242" + }, + "expiry_month": { + "type": "string", + "description": "The card's expiry month" + }, + "expiry_year": { + "type": "string", + "description": "The card's expiry year" + }, + "card_holder_name": { + "type": "string", + "description": "The card holder's name", + "example": "John Doe" + } + } + }, + "ApplePayDecryptAdditionalData": { + "type": "object", + "description": "Masked payout method details for Apple pay decrypt wallet payout method", + "required": [ + "card_exp_month", + "card_exp_year", + "card_holder_name" + ], + "properties": { + "card_exp_month": { + "type": "string", + "description": "Card expiry month", + "example": "01" + }, + "card_exp_year": { + "type": "string", + "description": "Card expiry year", + "example": "2026" + }, + "card_holder_name": { + "type": "string", + "description": "Card holder name", + "example": "John Doe" + } + } + }, "ApplePayPaymentData": { "oneOf": [ { @@ -26991,7 +27046,8 @@ "payone", "paypal", "stripe", - "wise" + "wise", + "worldpay" ] }, "PayoutCreatePayoutLinkConfig": { @@ -33457,6 +33513,17 @@ }, "Wallet": { "oneOf": [ + { + "type": "object", + "required": [ + "apple_pay_decrypt" + ], + "properties": { + "apple_pay_decrypt": { + "$ref": "#/components/schemas/ApplePayDecrypt" + } + } + }, { "type": "object", "required": [ @@ -33488,6 +33555,9 @@ }, { "$ref": "#/components/schemas/VenmoAdditionalData" + }, + { + "$ref": "#/components/schemas/ApplePayDecryptAdditionalData" } ], "description": "Masked payout method details for wallet payout method" diff --git a/api-reference/v2/openapi_spec_v2.json b/api-reference/v2/openapi_spec_v2.json index 7a6803d508..d56065e6dc 100644 --- a/api-reference/v2/openapi_spec_v2.json +++ b/api-reference/v2/openapi_spec_v2.json @@ -4808,6 +4808,61 @@ } } }, + "ApplePayDecrypt": { + "type": "object", + "required": [ + "dpan", + "expiry_month", + "expiry_year", + "card_holder_name" + ], + "properties": { + "dpan": { + "type": "string", + "description": "The dpan number associated with card number", + "example": "4242424242424242" + }, + "expiry_month": { + "type": "string", + "description": "The card's expiry month" + }, + "expiry_year": { + "type": "string", + "description": "The card's expiry year" + }, + "card_holder_name": { + "type": "string", + "description": "The card holder's name", + "example": "John Doe" + } + } + }, + "ApplePayDecryptAdditionalData": { + "type": "object", + "description": "Masked payout method details for Apple pay decrypt wallet payout method", + "required": [ + "card_exp_month", + "card_exp_year", + "card_holder_name" + ], + "properties": { + "card_exp_month": { + "type": "string", + "description": "Card expiry month", + "example": "01" + }, + "card_exp_year": { + "type": "string", + "description": "Card expiry year", + "example": "2026" + }, + "card_holder_name": { + "type": "string", + "description": "Card holder name", + "example": "John Doe" + } + } + }, "ApplePayPaymentData": { "oneOf": [ { @@ -21075,7 +21130,8 @@ "payone", "paypal", "stripe", - "wise" + "wise", + "worldpay" ] }, "PayoutCreatePayoutLinkConfig": { @@ -26999,6 +27055,17 @@ }, "Wallet": { "oneOf": [ + { + "type": "object", + "required": [ + "apple_pay_decrypt" + ], + "properties": { + "apple_pay_decrypt": { + "$ref": "#/components/schemas/ApplePayDecrypt" + } + } + }, { "type": "object", "required": [ @@ -27030,6 +27097,9 @@ }, { "$ref": "#/components/schemas/VenmoAdditionalData" + }, + { + "$ref": "#/components/schemas/ApplePayDecryptAdditionalData" } ], "description": "Masked payout method details for wallet payout method" diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index ed1bf12a24..1f3f5bdd15 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -56,6 +56,7 @@ pub enum PayoutConnectors { Paypal, Stripe, Wise, + Worldpay, } #[cfg(feature = "v2")] @@ -85,6 +86,7 @@ impl From for RoutableConnectors { PayoutConnectors::Paypal => Self::Paypal, PayoutConnectors::Stripe => Self::Stripe, PayoutConnectors::Wise => Self::Wise, + PayoutConnectors::Worldpay => Self::Worldpay, } } } @@ -105,6 +107,7 @@ impl From for Connector { PayoutConnectors::Paypal => Self::Paypal, PayoutConnectors::Stripe => Self::Stripe, PayoutConnectors::Wise => Self::Wise, + PayoutConnectors::Worldpay => Self::Worldpay, } } } @@ -125,6 +128,7 @@ impl TryFrom for PayoutConnectors { Connector::Paypal => Ok(Self::Paypal), Connector::Stripe => Ok(Self::Stripe), Connector::Wise => Ok(Self::Wise), + Connector::Worldpay => Ok(Self::Worldpay), _ => Err(format!("Invalid payout connector {value}")), } } diff --git a/crates/api_models/src/payouts.rs b/crates/api_models/src/payouts.rs index 03b5380416..fc41273288 100644 --- a/crates/api_models/src/payouts.rs +++ b/crates/api_models/src/payouts.rs @@ -375,6 +375,7 @@ pub struct PixBankTransfer { #[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] #[serde(rename_all = "snake_case")] pub enum Wallet { + ApplePayDecrypt(ApplePayDecrypt), Paypal(Paypal), Venmo(Venmo), } @@ -414,6 +415,25 @@ pub struct Venmo { pub telephone_number: Option>, } +#[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +pub struct ApplePayDecrypt { + /// The dpan number associated with card number + #[schema(value_type = String, example = "4242424242424242")] + pub dpan: CardNumber, + + /// The card's expiry month + #[schema(value_type = String)] + pub expiry_month: Secret, + + /// The card's expiry year + #[schema(value_type = String)] + pub expiry_year: Secret, + + /// The card holder's name + #[schema(value_type = String, example = "John Doe")] + pub card_holder_name: Option>, +} + #[derive(Debug, ToSchema, Clone, Serialize, router_derive::PolymorphicSchema)] #[serde(deny_unknown_fields)] pub struct PayoutCreateResponse { @@ -1000,6 +1020,18 @@ impl From for payout_method_utils::WalletAdditionalData { telephone_number: telephone_number.map(From::from), })) } + Wallet::ApplePayDecrypt(ApplePayDecrypt { + expiry_month, + expiry_year, + card_holder_name, + .. + }) => Self::ApplePayDecrypt(Box::new( + payout_method_utils::ApplePayDecryptAdditionalData { + card_exp_month: expiry_month, + card_exp_year: expiry_year, + card_holder_name, + }, + )), } } } diff --git a/crates/common_enums/src/connector_enums.rs b/crates/common_enums/src/connector_enums.rs index 6604aa7a06..f6e7348e1e 100644 --- a/crates/common_enums/src/connector_enums.rs +++ b/crates/common_enums/src/connector_enums.rs @@ -379,6 +379,7 @@ impl Connector { | (Self::Adyenplatform, _) | (Self::Nomupay, _) | (Self::Loonio, _) + | (Self::Worldpay, Some(PayoutType::Wallet)) ) } #[cfg(feature = "payouts")] diff --git a/crates/common_utils/src/payout_method_utils.rs b/crates/common_utils/src/payout_method_utils.rs index 94f23c179d..1becc711f4 100644 --- a/crates/common_utils/src/payout_method_utils.rs +++ b/crates/common_utils/src/payout_method_utils.rs @@ -209,6 +209,8 @@ pub enum WalletAdditionalData { Paypal(Box), /// Additional data for venmo wallet payout method Venmo(Box), + /// Additional data for Apple pay decrypt wallet payout method + ApplePayDecrypt(Box), } /// Masked payout method details for paypal wallet payout method @@ -241,6 +243,25 @@ pub struct VenmoAdditionalData { pub telephone_number: Option, } +/// Masked payout method details for Apple pay decrypt wallet payout method +#[derive( + Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, FromSqlRow, AsExpression, ToSchema, +)] +#[diesel(sql_type = Jsonb)] +pub struct ApplePayDecryptAdditionalData { + /// Card expiry month + #[schema(value_type = String, example = "01")] + pub card_exp_month: Secret, + + /// Card expiry year + #[schema(value_type = String, example = "2026")] + pub card_exp_year: Secret, + + /// Card holder name + #[schema(value_type = String, example = "John Doe")] + pub card_holder_name: Option>, +} + /// Masked payout method details for wallet payout method #[derive( Eq, PartialEq, Clone, Debug, Deserialize, Serialize, FromSqlRow, AsExpression, ToSchema, diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index 9ddf6a1846..e105fab53b 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -362,6 +362,8 @@ pub struct ConnectorConfig { pub wise_payout: Option, pub worldline: Option, pub worldpay: Option, + #[cfg(feature = "payouts")] + pub worldpay_payout: Option, pub worldpayvantiv: Option, pub worldpayxml: Option, pub xendit: Option, @@ -412,6 +414,7 @@ impl ConnectorConfig { PayoutConnectors::Paypal => Ok(connector_data.paypal_payout), PayoutConnectors::Stripe => Ok(connector_data.stripe_payout), PayoutConnectors::Wise => Ok(connector_data.wise_payout), + PayoutConnectors::Worldpay => Ok(connector_data.worldpay_payout), } } diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index 6c87d88f13..e6ecce7b30 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -5210,6 +5210,21 @@ required=true type="MultiSelect" options=["PAN_ONLY", "CRYPTOGRAM_3DS"] +[[worldpay_payout.wallet]] + payment_method_type = "apple_pay" +[worldpay_payout.connector_auth.SignatureKey] +key1="Username" +api_key="Password" +api_secret="Merchant Identifier" +[worldpay_payout.connector_webhook_details] +merchant_secret="Source verification key" +[worldpay_payout.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" + [zen] [[zen.credit]] diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index 5962c55929..787801f797 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -3977,6 +3977,20 @@ required = true type = "MultiSelect" options = ["PAN_ONLY", "CRYPTOGRAM_3DS"] +[[worldpay_payout.wallet]] + payment_method_type = "apple_pay" +[worldpay_payout.connector_auth.SignatureKey] +key1="Username" +api_key="Password" +api_secret="Merchant Identifier" +[worldpay_payout.connector_webhook_details] +merchant_secret="Source verification key" +[worldpay_payout.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" [payme] [[payme.credit]] diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index 7b79421cc0..1ae6e5be26 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -5169,6 +5169,20 @@ required = true type = "MultiSelect" options = ["PAN_ONLY", "CRYPTOGRAM_3DS"] +[[worldpay_payout.wallet]] + payment_method_type = "apple_pay" +[worldpay_payout.connector_auth.SignatureKey] +key1="Username" +api_key="Password" +api_secret="Merchant Identifier" +[worldpay_payout.connector_webhook_details] +merchant_secret="Source verification key" +[worldpay_payout.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" [zen] [[zen.credit]] diff --git a/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs b/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs index e455882151..ebe056e39a 100644 --- a/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs @@ -5711,6 +5711,12 @@ impl TryFrom<&AdyenRouterData<&PayoutsRouterData>> for AdyenPayoutCreateRe message: "Venmo Wallet is not supported".to_string(), connector: "Adyen", })?, + payouts::Wallet::ApplePayDecrypt(_) => { + Err(errors::ConnectorError::NotSupported { + message: "Apple Pay Decrypt Wallet is not supported".to_string(), + connector: "Adyen", + })? + } }; let address: &hyperswitch_domain_models::address::AddressDetails = item.router_data.get_billing_address()?; diff --git a/crates/hyperswitch_connectors/src/connectors/paypal/transformers.rs b/crates/hyperswitch_connectors/src/connectors/paypal/transformers.rs index 23d06999d6..2f27e8b174 100644 --- a/crates/hyperswitch_connectors/src/connectors/paypal/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/paypal/transformers.rs @@ -2544,6 +2544,10 @@ impl TryFrom<&PaypalRouterData<&PayoutsRouterData>> for PaypalPayoutI receiver, } } + WalletPayout::ApplePayDecrypt(_) => Err(errors::ConnectorError::NotSupported { + message: "ApplePayDecrypt PayoutMethodType is not supported".to_string(), + connector: "Paypal", + })?, }, _ => Err(errors::ConnectorError::NotSupported { message: "PayoutMethodType is not supported".to_string(), diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay.rs b/crates/hyperswitch_connectors/src/connectors/worldpay.rs index 9883549ad9..066b8984a1 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldpay.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldpay.rs @@ -1,3 +1,9 @@ +#[cfg(feature = "payouts")] +mod payout_requests; +#[cfg(feature = "payouts")] +mod payout_response; +#[cfg(feature = "payouts")] +pub mod payout_transformers; mod requests; mod response; pub mod transformers; @@ -38,6 +44,11 @@ use hyperswitch_domain_models::{ RefundSyncRouterData, RefundsRouterData, SetupMandateRouterData, }, }; +#[cfg(feature = "payouts")] +use hyperswitch_domain_models::{ + router_flow_types::payouts::PoFulfill, router_request_types::PayoutsData, + router_response_types::PayoutsResponseData, types::PayoutsRouterData, +}; use hyperswitch_interfaces::{ api::{ self, ConnectorCommon, ConnectorCommonExt, ConnectorIntegration, ConnectorRedirectResponse, @@ -50,6 +61,10 @@ use hyperswitch_interfaces::{ webhooks::{IncomingWebhook, IncomingWebhookRequestDetails}, }; use masking::Mask; +#[cfg(feature = "payouts")] +use payout_requests::WorldpayPayoutRequest; +#[cfg(feature = "payouts")] +use payout_response::WorldpayPayoutResponse; use requests::{ WorldpayCompleteAuthorizationRequest, WorldpayPartialRequest, WorldpayPaymentsRequest, }; @@ -60,6 +75,8 @@ use response::{ }; use ring::hmac; +#[cfg(feature = "payouts")] +use self::payout_transformers as worldpay_payout; use self::transformers as worldpay; use crate::{ constants::headers, @@ -70,6 +87,9 @@ use crate::{ }, }; +#[cfg(feature = "payouts")] +const WORLDPAY_PAYOUT_CONTENT_TYPE: &str = "application/vnd.worldpay.payouts-v4+json"; + #[derive(Clone)] pub struct Worldpay { amount_converter: &'static (dyn AmountConvertor + Sync), @@ -1087,6 +1107,124 @@ impl ConnectorIntegration for Worldpay } } +impl api::Payouts for Worldpay {} +#[cfg(feature = "payouts")] +impl api::PayoutFulfill for Worldpay {} + +#[async_trait::async_trait] +#[cfg(feature = "payouts")] +impl ConnectorIntegration for Worldpay { + fn get_url( + &self, + _req: &PayoutsRouterData, + connectors: &Connectors, + ) -> CustomResult { + Ok(format!( + "{}payouts/basicDisbursement", + ConnectorCommon::base_url(self, connectors) + )) + } + + fn get_headers( + &self, + req: &PayoutsRouterData, + _connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let auth = worldpay_payout::WorldpayPayoutAuthType::try_from(&req.connector_auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let headers = vec![ + ( + headers::AUTHORIZATION.to_string(), + auth.api_key.into_masked(), + ), + ( + headers::ACCEPT.to_string(), + WORLDPAY_PAYOUT_CONTENT_TYPE.to_string().into(), + ), + ( + headers::CONTENT_TYPE.to_string(), + WORLDPAY_PAYOUT_CONTENT_TYPE.to_string().into(), + ), + (headers::WP_API_VERSION.to_string(), "2024-06-01".into()), + ]; + + Ok(headers) + } + + fn get_request_body( + &self, + req: &PayoutsRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let connector_router_data = worldpay_payout::WorldpayPayoutRouterData::try_from(( + &self.get_currency_unit(), + req.request.destination_currency, + req.request.minor_amount, + req, + ))?; + let auth = worldpay_payout::WorldpayPayoutAuthType::try_from(&req.connector_auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let connector_req = + WorldpayPayoutRequest::try_from((&connector_router_data, &auth.entity_id))?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &PayoutsRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = RequestBuilder::new() + .method(Method::Post) + .url(&types::PayoutFulfillType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PayoutFulfillType::get_headers( + self, req, connectors, + )?) + .set_body(types::PayoutFulfillType::get_request_body( + self, req, connectors, + )?) + .build(); + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &PayoutsRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult, errors::ConnectorError> { + let response: WorldpayPayoutResponse = res + .response + .parse_struct("WorldpayPayoutResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + 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 { + self.build_error_response(res, event_builder) + } + + fn get_5xx_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + #[async_trait::async_trait] impl IncomingWebhook for Worldpay { fn get_webhook_source_verification_algorithm( diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay/payout_requests.rs b/crates/hyperswitch_connectors/src/connectors/worldpay/payout_requests.rs new file mode 100644 index 0000000000..c9247ffd6c --- /dev/null +++ b/crates/hyperswitch_connectors/src/connectors/worldpay/payout_requests.rs @@ -0,0 +1,66 @@ +use masking::Secret; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WorldpayPayoutRequest { + pub transaction_reference: String, + pub merchant: Merchant, + pub instruction: PayoutInstruction, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PayoutInstruction { + pub payout_instrument: PayoutInstrument, + pub narrative: InstructionNarrative, + pub value: PayoutValue, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PayoutValue { + pub amount: i64, + pub currency: api_models::enums::Currency, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Merchant { + pub entity: Secret, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InstructionNarrative { + pub line1: Secret, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum PayoutInstrument { + ApplePayDecrypt(ApplePayDecrypt), +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplePayDecrypt { + #[serde(rename = "type")] + pub payout_type: PayoutType, + pub dpan: cards::CardNumber, + pub card_expiry_date: PayoutExpiryDate, + #[serde(skip_serializing_if = "Option::is_none")] + pub card_holder_name: Option>, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct PayoutExpiryDate { + pub month: Secret, + pub year: Secret, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum PayoutType { + #[serde(rename = "card/networkToken+applepay")] + ApplePayDecrypt, +} diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay/payout_response.rs b/crates/hyperswitch_connectors/src/connectors/worldpay/payout_response.rs new file mode 100644 index 0000000000..2c5d948471 --- /dev/null +++ b/crates/hyperswitch_connectors/src/connectors/worldpay/payout_response.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WorldpayPayoutResponse { + pub outcome: PayoutOutcome, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum PayoutOutcome { + RequestReceived, + Refused, + Error, + QueryRequired, +} diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay/payout_transformers.rs b/crates/hyperswitch_connectors/src/connectors/worldpay/payout_transformers.rs new file mode 100644 index 0000000000..873dc8100c --- /dev/null +++ b/crates/hyperswitch_connectors/src/connectors/worldpay/payout_transformers.rs @@ -0,0 +1,193 @@ +use base64::Engine; +use common_enums::enums; +use common_utils::{consts::BASE64_ENGINE, pii, types::MinorUnit}; +use error_stack::ResultExt; +use hyperswitch_domain_models::{ + router_data::ConnectorAuthType, router_flow_types::payouts::PoFulfill, + router_response_types::PayoutsResponseData, types, +}; +use hyperswitch_interfaces::{api, errors}; +use masking::{PeekInterface, Secret}; +use serde::{Deserialize, Serialize}; + +use super::{payout_requests::*, payout_response::*}; +use crate::{ + types::PayoutsResponseRouterData, + utils::{self, CardData, RouterData as RouterDataTrait}, +}; + +#[derive(Debug, Serialize)] +pub struct WorldpayPayoutRouterData { + amount: i64, + router_data: T, +} +impl TryFrom<(&api::CurrencyUnit, enums::Currency, MinorUnit, T)> + for WorldpayPayoutRouterData +{ + type Error = error_stack::Report; + fn try_from( + (_currency_unit, _currency, minor_amount, item): ( + &api::CurrencyUnit, + enums::Currency, + MinorUnit, + T, + ), + ) -> Result { + Ok(Self { + amount: minor_amount.get_amount_as_i64(), + router_data: item, + }) + } +} + +pub struct WorldpayPayoutAuthType { + pub(super) api_key: Secret, + pub(super) entity_id: Secret, +} + +impl TryFrom<&ConnectorAuthType> for WorldpayPayoutAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &ConnectorAuthType) -> Result { + match auth_type { + ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } => { + let auth_key = format!("{}:{}", key1.peek(), api_key.peek()); + let auth_header = format!("Basic {}", BASE64_ENGINE.encode(auth_key)); + Ok(Self { + api_key: Secret::new(auth_header), + entity_id: api_secret.clone(), + }) + } + _ => Err(errors::ConnectorError::FailedToObtainAuthType)?, + } + } +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct WorldpayPayoutConnectorMetadataObject { + pub merchant_name: Option>, +} + +impl TryFrom> for WorldpayPayoutConnectorMetadataObject { + type Error = error_stack::Report; + fn try_from(meta_data: Option<&pii::SecretSerdeValue>) -> Result { + let metadata: Self = utils::to_connector_meta_from_secret::(meta_data.cloned()) + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "metadata", + })?; + Ok(metadata) + } +} + +impl + TryFrom<( + &WorldpayPayoutRouterData<&types::PayoutsRouterData>, + &Secret, + )> for WorldpayPayoutRequest +{ + type Error = error_stack::Report; + + fn try_from( + req: ( + &WorldpayPayoutRouterData<&types::PayoutsRouterData>, + &Secret, + ), + ) -> Result { + let (item, entity_id) = req; + + let worldpay_connector_metadata_object: WorldpayPayoutConnectorMetadataObject = + WorldpayPayoutConnectorMetadataObject::try_from( + item.router_data.connector_meta_data.as_ref(), + )?; + + let merchant_name = worldpay_connector_metadata_object.merchant_name.ok_or( + errors::ConnectorError::InvalidConnectorConfig { + config: "metadata.merchant_name", + }, + )?; + + Ok(Self { + transaction_reference: item.router_data.connector_request_reference_id.clone(), + merchant: Merchant { + entity: entity_id.clone(), + }, + instruction: PayoutInstruction { + value: PayoutValue { + amount: item.amount, + currency: item.router_data.request.destination_currency, + }, + narrative: InstructionNarrative { + line1: merchant_name, + }, + payout_instrument: PayoutInstrument::try_from( + item.router_data.get_payout_method_data()?, + )?, + }, + }) + } +} + +impl TryFrom for PayoutInstrument { + type Error = error_stack::Report; + + fn try_from( + payout_method_data: api_models::payouts::PayoutMethodData, + ) -> Result { + match payout_method_data { + api_models::payouts::PayoutMethodData::Wallet( + api_models::payouts::Wallet::ApplePayDecrypt(apple_pay_decrypted_data), + ) => Ok(Self::ApplePayDecrypt(ApplePayDecrypt { + payout_type: PayoutType::ApplePayDecrypt, + dpan: apple_pay_decrypted_data.dpan.clone(), + card_holder_name: apple_pay_decrypted_data.card_holder_name.clone(), + card_expiry_date: PayoutExpiryDate { + month: apple_pay_decrypted_data.get_expiry_month_as_i8()?, + year: apple_pay_decrypted_data.get_expiry_year_as_4_digit_i32()?, + }, + })), + api_models::payouts::PayoutMethodData::Card(_) + | api_models::payouts::PayoutMethodData::Bank(_) + | api_models::payouts::PayoutMethodData::Wallet(_) + | api_models::payouts::PayoutMethodData::BankRedirect(_) => { + Err(errors::ConnectorError::NotImplemented( + "Selected Payout Method is not implemented for Worldpay".to_string(), + ) + .into()) + } + } + } +} + +impl From for enums::PayoutStatus { + fn from(item: PayoutOutcome) -> Self { + match item { + PayoutOutcome::RequestReceived => Self::Initiated, + PayoutOutcome::Error | PayoutOutcome::Refused => Self::Failed, + PayoutOutcome::QueryRequired => Self::Pending, + } + } +} + +impl TryFrom> + for types::PayoutsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: PayoutsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(PayoutsResponseData { + status: Some(enums::PayoutStatus::from(item.response.outcome.clone())), + connector_payout_id: None, + payout_eligible: None, + should_add_next_step_to_process_tracker: false, + error_code: None, + error_message: None, + }), + ..item.data + }) + } +} diff --git a/crates/hyperswitch_connectors/src/default_implementations.rs b/crates/hyperswitch_connectors/src/default_implementations.rs index f143073a03..6aee074756 100644 --- a/crates/hyperswitch_connectors/src/default_implementations.rs +++ b/crates/hyperswitch_connectors/src/default_implementations.rs @@ -3914,7 +3914,6 @@ default_imp_for_payouts!( connectors::UnifiedAuthenticationService, connectors::Volt, connectors::Worldline, - connectors::Worldpay, connectors::Worldpayvantiv, connectors::Worldpayxml, connectors::Wellsfargo, @@ -4494,7 +4493,6 @@ default_imp_for_payouts_fulfill!( connectors::Tsys, connectors::UnifiedAuthenticationService, connectors::Worldline, - connectors::Worldpay, connectors::Worldpayvantiv, connectors::Worldpayxml, connectors::Wellsfargo, diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index b256ca82ec..68256b5353 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -1346,6 +1346,115 @@ impl CardData for CardDetailsForNetworkTransactionId { } } +#[cfg(feature = "payouts")] +impl CardData for api_models::payouts::ApplePayDecrypt { + fn get_card_expiry_year_2_digit(&self) -> Result, errors::ConnectorError> { + let binding = self.expiry_month.clone(); + let year = binding.peek(); + Ok(Secret::new( + year.get(year.len() - 2..) + .ok_or(errors::ConnectorError::RequestEncodingFailed)? + .to_string(), + )) + } + fn get_card_expiry_month_2_digit(&self) -> Result, errors::ConnectorError> { + let exp_month = self + .expiry_month + .peek() + .to_string() + .parse::() + .map_err(|_| errors::ConnectorError::InvalidDataFormat { + field_name: "payout_method_data.apple_pay_decrypt.expiry_month", + })?; + let month = ::cards::CardExpirationMonth::try_from(exp_month).map_err(|_| { + errors::ConnectorError::InvalidDataFormat { + field_name: "payout_method_data.apple_pay_decrypt.expiry_month", + } + })?; + Ok(Secret::new(month.two_digits())) + } + fn get_card_issuer(&self) -> Result { + Err(errors::ConnectorError::ParsingFailed) + .attach_printable("get_card_issuer is not supported for Applepay Decrypted Payout") + } + fn get_card_expiry_month_year_2_digit_with_delimiter( + &self, + delimiter: String, + ) -> Result, errors::ConnectorError> { + let year = self.get_card_expiry_year_2_digit()?; + Ok(Secret::new(format!( + "{}{}{}", + self.expiry_month.peek(), + delimiter, + year.peek() + ))) + } + fn get_expiry_date_as_yyyymm(&self, delimiter: &str) -> Secret { + let year = self.get_expiry_year_4_digit(); + Secret::new(format!( + "{}{}{}", + year.peek(), + delimiter, + self.expiry_month.peek() + )) + } + fn get_expiry_date_as_mmyyyy(&self, delimiter: &str) -> Secret { + let year = self.get_expiry_year_4_digit(); + Secret::new(format!( + "{}{}{}", + self.expiry_month.peek(), + delimiter, + year.peek() + )) + } + fn get_expiry_year_4_digit(&self) -> Secret { + let mut year = self.expiry_year.peek().clone(); + if year.len() == 2 { + year = format!("20{year}"); + } + Secret::new(year) + } + fn get_expiry_date_as_yymm(&self) -> Result, errors::ConnectorError> { + let year = self.get_card_expiry_year_2_digit()?.expose(); + let month = self.expiry_month.clone().expose(); + Ok(Secret::new(format!("{year}{month}"))) + } + fn get_expiry_date_as_mmyy(&self) -> Result, errors::ConnectorError> { + let year = self.get_card_expiry_year_2_digit()?.expose(); + let month = self.expiry_month.clone().expose(); + Ok(Secret::new(format!("{month}{year}"))) + } + fn get_expiry_month_as_i8(&self) -> Result, Error> { + self.expiry_month + .peek() + .clone() + .parse::() + .change_context(errors::ConnectorError::ResponseDeserializationFailed) + .map(Secret::new) + } + fn get_expiry_year_as_i32(&self) -> Result, Error> { + self.expiry_year + .peek() + .clone() + .parse::() + .change_context(errors::ConnectorError::ResponseDeserializationFailed) + .map(Secret::new) + } + fn get_expiry_year_as_4_digit_i32(&self) -> Result, Error> { + self.get_expiry_year_4_digit() + .peek() + .clone() + .parse::() + .change_context(errors::ConnectorError::ResponseDeserializationFailed) + .map(Secret::new) + } + fn get_cardholder_name(&self) -> Result, Error> { + self.card_holder_name + .clone() + .ok_or_else(missing_field_err("card.card_holder_name")) + } +} + #[track_caller] fn get_card_issuer(card_number: &str) -> Result { for (k, v) in CARD_REGEX.iter() { diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index 430fb7f0e3..7220e6c8a9 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -240,6 +240,7 @@ Never share your secret api keys. Keep them guarded and secure. common_utils::payout_method_utils::PaypalAdditionalData, common_utils::payout_method_utils::InteracAdditionalData, common_utils::payout_method_utils::VenmoAdditionalData, + common_utils::payout_method_utils::ApplePayDecryptAdditionalData, common_types::payments::SplitPaymentsRequest, common_types::payments::GpayTokenizationData, common_types::payments::GPayPredecryptData, @@ -690,6 +691,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payouts::PayoutMethodDataResponse, api_models::payouts::PayoutLinkResponse, api_models::payouts::Bank, + api_models::payouts::ApplePayDecrypt, api_models::payouts::PayoutCreatePayoutLinkConfig, api_models::enums::PayoutEntityType, api_models::enums::PayoutSendPriority, diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index b14a729c0b..6aa19875c0 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -183,6 +183,7 @@ Never share your secret api keys. Keep them guarded and secure. common_utils::payout_method_utils::PaypalAdditionalData, common_utils::payout_method_utils::InteracAdditionalData, common_utils::payout_method_utils::VenmoAdditionalData, + common_utils::payout_method_utils::ApplePayDecryptAdditionalData, common_types::payments::SplitPaymentsRequest, common_types::payments::GpayTokenizationData, common_types::payments::GPayPredecryptData, @@ -661,6 +662,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payouts::PayoutMethodDataResponse, api_models::payouts::PayoutLinkResponse, api_models::payouts::Bank, + api_models::payouts::ApplePayDecrypt, api_models::payouts::PayoutCreatePayoutLinkConfig, api_models::enums::PayoutEntityType, api_models::enums::PayoutSendPriority, diff --git a/crates/router/src/core/payment_methods/vault.rs b/crates/router/src/core/payment_methods/vault.rs index 1b99c1ab33..81a3cfe849 100644 --- a/crates/router/src/core/payment_methods/vault.rs +++ b/crates/router/src/core/payment_methods/vault.rs @@ -551,6 +551,10 @@ pub struct TokenizedWalletSensitiveValues { pub telephone_number: Option>, pub wallet_id: Option>, pub wallet_type: PaymentMethodType, + pub dpan: Option, + pub expiry_month: Option>, + pub expiry_year: Option>, + pub card_holder_name: Option>, } #[derive(Debug, serde::Serialize, serde::Deserialize)] @@ -570,12 +574,30 @@ impl Vaultable for api::WalletPayout { telephone_number: paypal_data.telephone_number.clone(), wallet_id: paypal_data.paypal_id.clone(), wallet_type: PaymentMethodType::Paypal, + dpan: None, + expiry_month: None, + expiry_year: None, + card_holder_name: None, }, Self::Venmo(venmo_data) => TokenizedWalletSensitiveValues { email: None, telephone_number: venmo_data.telephone_number.clone(), wallet_id: None, wallet_type: PaymentMethodType::Venmo, + dpan: None, + expiry_month: None, + expiry_year: None, + card_holder_name: None, + }, + Self::ApplePayDecrypt(apple_pay_decrypt_data) => TokenizedWalletSensitiveValues { + email: None, + telephone_number: None, + wallet_id: None, + wallet_type: PaymentMethodType::ApplePay, + dpan: Some(apple_pay_decrypt_data.dpan.clone()), + expiry_month: Some(apple_pay_decrypt_data.expiry_month.clone()), + expiry_year: Some(apple_pay_decrypt_data.expiry_year.clone()), + card_holder_name: apple_pay_decrypt_data.card_holder_name.clone(), }, }; @@ -620,6 +642,19 @@ impl Vaultable for api::WalletPayout { PaymentMethodType::Venmo => Self::Venmo(api_models::payouts::Venmo { telephone_number: value1.telephone_number, }), + PaymentMethodType::ApplePay => { + match (value1.dpan, value1.expiry_month, value1.expiry_year) { + (Some(dpan), Some(expiry_month), Some(expiry_year)) => { + Self::ApplePayDecrypt(api_models::payouts::ApplePayDecrypt { + dpan, + expiry_month, + expiry_year, + card_holder_name: value1.card_holder_name, + }) + } + _ => Err(errors::VaultError::ResponseDeserializationFailed)?, + } + } _ => Err(errors::VaultError::PayoutMethodNotSupported)?, }; let supp_data = SupplementaryVaultData { diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 910ec21335..90e25b34cf 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -1220,6 +1220,7 @@ impl ForeignFrom<&api_models::payouts::Wallet> for api_enums::PaymentMethodType match value { api_models::payouts::Wallet::Paypal(_) => Self::Paypal, api_models::payouts::Wallet::Venmo(_) => Self::Venmo, + api_models::payouts::Wallet::ApplePayDecrypt(_) => Self::ApplePay, } } }