Skip to content

Commit

Permalink
feat(connector): [Paypal] implement vaulting for paypal cards via zer…
Browse files Browse the repository at this point in the history
…o mandates (#5324)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
Co-authored-by: swangi-kumari <[email protected]>
  • Loading branch information
3 people authored Nov 25, 2024
1 parent 62dc4e6 commit 83e8bc0
Show file tree
Hide file tree
Showing 3 changed files with 260 additions and 31 deletions.
81 changes: 70 additions & 11 deletions crates/router/src/connector/paypal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -652,19 +652,78 @@ impl
types::PaymentsResponseData,
> for Paypal
{
fn build_request(
fn get_headers(
&self,
_req: &types::RouterData<
api::SetupMandate,
types::SetupMandateRequestData,
types::PaymentsResponseData,
>,
req: &types::SetupMandateRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_url(
&self,
_req: &types::SetupMandateRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}v3/vault/payment-tokens/",
self.base_url(connectors)
))
}
fn get_request_body(
&self,
req: &types::SetupMandateRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Err(
errors::ConnectorError::NotImplemented("Setup Mandate flow for Paypal".to_string())
.into(),
)
) -> CustomResult<RequestContent, errors::ConnectorError> {
let connector_req = paypal::PaypalZeroMandateRequest::try_from(req)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}

fn build_request(
&self,
req: &types::SetupMandateRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<common_utils::request::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::SetupMandateType::get_url(self, req, connectors)?)
.attach_default_headers()
.headers(types::SetupMandateType::get_headers(self, req, connectors)?)
.set_body(types::SetupMandateType::get_request_body(
self, req, connectors,
)?)
.build(),
))
}

fn handle_response(
&self,
data: &types::SetupMandateRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<types::SetupMandateRouterData, errors::ConnectorError> {
let response: paypal::PaypalSetupMandatesResponse = res
.response
.parse_struct("PaypalSetupMandatesResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}

fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
}

Expand Down
141 changes: 133 additions & 8 deletions crates/router/src/connector/paypal/transformers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,21 +425,21 @@ pub struct RedirectRequest {
experience_context: ContextStruct,
}

#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct ContextStruct {
return_url: Option<String>,
cancel_url: Option<String>,
user_action: Option<UserAction>,
shipping_preference: ShippingPreference,
}

#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, Deserialize)]
pub enum UserAction {
#[serde(rename = "PAY_NOW")]
PayNow,
}

#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, Deserialize)]
pub enum ShippingPreference {
#[serde(rename = "SET_PROVIDED_ADDRESS")]
SetProvidedAddress,
Expand Down Expand Up @@ -527,6 +527,132 @@ pub struct PaypalPaymentsRequest {
payment_source: Option<PaymentSourceItem>,
}

#[derive(Debug, Serialize)]
pub struct PaypalZeroMandateRequest {
payment_source: ZeroMandateSourceItem,
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ZeroMandateSourceItem {
Card(CardMandateRequest),
Paypal(PaypalMandateStruct),
}

#[derive(Debug, Serialize, Deserialize)]
pub struct PaypalMandateStruct {
experience_context: Option<ContextStruct>,
usage_type: UsageType,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct CardMandateRequest {
billing_address: Option<Address>,
expiry: Option<Secret<String>>,
name: Option<Secret<String>>,
number: Option<cards::CardNumber>,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct PaypalSetupMandatesResponse {
id: String,
customer: Customer,
payment_source: ZeroMandateSourceItem,
links: Vec<PaypalLinks>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Customer {
id: String,
}

impl<F, T>
TryFrom<
types::ResponseRouterData<F, PaypalSetupMandatesResponse, T, types::PaymentsResponseData>,
> for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<
F,
PaypalSetupMandatesResponse,
T,
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
let info_response = item.response;

let mandate_reference = Some(MandateReference {
connector_mandate_id: Some(info_response.id.clone()),
payment_method_id: None,
mandate_metadata: None,
connector_mandate_request_reference_id: None,
});
// https://developer.paypal.com/docs/api/payment-tokens/v3/#payment-tokens_create
// If 201 status code, then order is captured, other status codes are handled by the error handler
let status = if item.http_code == 201 {
enums::AttemptStatus::Charged
} else {
enums::AttemptStatus::Failure
};
Ok(Self {
status,
return_url: None,
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(info_response.id.clone()),
redirection_data: Box::new(None),
mandate_reference: Box::new(mandate_reference),
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: Some(info_response.id.clone()),
incremental_authorization_allowed: None,
charge_id: None,
}),
..item.data
})
}
}
impl TryFrom<&types::SetupMandateRouterData> for PaypalZeroMandateRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::SetupMandateRouterData) -> Result<Self, Self::Error> {
let payment_source = match item.request.payment_method_data.clone() {
domain::PaymentMethodData::Card(ccard) => {
ZeroMandateSourceItem::Card(CardMandateRequest {
billing_address: get_address_info(item.get_optional_billing()),
expiry: Some(ccard.get_expiry_date_as_yyyymm("-")),
name: item.get_optional_billing_full_name(),
number: Some(ccard.card_number),
})
}

domain::PaymentMethodData::Wallet(_)
| domain::PaymentMethodData::CardRedirect(_)
| domain::PaymentMethodData::PayLater(_)
| domain::PaymentMethodData::BankRedirect(_)
| domain::PaymentMethodData::BankDebit(_)
| domain::PaymentMethodData::BankTransfer(_)
| domain::PaymentMethodData::Crypto(_)
| domain::PaymentMethodData::MandatePayment
| domain::PaymentMethodData::Reward
| domain::PaymentMethodData::RealTimePayment(_)
| domain::PaymentMethodData::Upi(_)
| domain::PaymentMethodData::Voucher(_)
| domain::PaymentMethodData::GiftCard(_)
| domain::PaymentMethodData::CardToken(_)
| domain::PaymentMethodData::CardDetailsForNetworkTransactionId(_)
| domain::PaymentMethodData::NetworkToken(_)
| domain::PaymentMethodData::OpenBanking(_)
| domain::PaymentMethodData::MobilePayment(_) => {
Err(errors::ConnectorError::NotImplemented(
utils::get_unimplemented_payment_method_error_message("Paypal"),
))?
}
};

Ok(Self { payment_source })
}
}

fn get_address_info(payment_address: Option<&api_models::payments::Address>) -> Option<Address> {
let address = payment_address.and_then(|payment_address| payment_address.address.as_ref());
match address {
Expand Down Expand Up @@ -973,11 +1099,11 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP
)?;

let payment_source = match payment_method_type {
enums::PaymentMethodType::Credit => Ok(Some(PaymentSourceItem::Card(
CardRequest::CardVaultStruct(VaultStruct {
enums::PaymentMethodType::Credit | enums::PaymentMethodType::Debit => Ok(Some(
PaymentSourceItem::Card(CardRequest::CardVaultStruct(VaultStruct {
vault_id: connector_mandate_id.into(),
}),
))),
})),
)),
enums::PaymentMethodType::Paypal => Ok(Some(PaymentSourceItem::Paypal(
PaypalRedirectionRequest::PaypalVaultStruct(VaultStruct {
vault_id: connector_mandate_id.into(),
Expand Down Expand Up @@ -1009,7 +1135,6 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP
| enums::PaymentMethodType::Cashapp
| enums::PaymentMethodType::Dana
| enums::PaymentMethodType::DanamonVa
| enums::PaymentMethodType::Debit
| enums::PaymentMethodType::DirectCarrierBilling
| enums::PaymentMethodType::DuitNow
| enums::PaymentMethodType::Efecty
Expand Down
69 changes: 57 additions & 12 deletions cypress-tests/cypress/e2e/PaymentUtils/Paypal.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,23 @@ const successfulThreeDSTestCardDetails = {
card_cvc: "123",
};

const singleUseMandateData = {
customer_acceptance: {
acceptance_type: "offline",
accepted_at: "1963-05-03T04:07:52.723Z",
online: {
ip_address: "125.0.0.1",
user_agent: "amet irure esse",
},
},
mandate_type: {
single_use: {
amount: 8000,
currency: "USD",
},
},
};

export const connectorDetails = {
card_pm: {
PaymentIntent: {
Expand Down Expand Up @@ -222,14 +239,18 @@ export const connectorDetails = {
},
},
ZeroAuthMandate: {
Request: {
payment_method: "card",
payment_method_data: {
card: successfulNo3DSCardDetails,
},
currency: "USD",
mandate_data: singleUseMandateData,
},
Response: {
status: 501,
status: 200,
body: {
error: {
type: "invalid_request",
message: "Setup Mandate flow for Paypal is not implemented",
code: "IR_00",
},
status: "succeeded",
},
},
},
Expand Down Expand Up @@ -257,13 +278,37 @@ export const connectorDetails = {
},
},
Response: {
status: 501,
status: 200,
body: {
error: {
type: "invalid_request",
message: "Setup Mandate flow for Paypal is not implemented",
code: "IR_00",
},
status: "succeeded",
setup_future_usage: "off_session",
},
},
},
SaveCardConfirmAutoCaptureOffSession: {
Request: {
setup_future_usage: "off_session",
},
Response: {
status: 200,
body: {
status: "succeeded",
},
},
},
PaymentIntentOffSession: {
Request: {
currency: "USD",
amount: 6500,
authentication_type: "no_three_ds",
customer_acceptance: null,
setup_future_usage: "off_session",
},
Response: {
status: 200,
body: {
status: "requires_payment_method",
setup_future_usage: "off_session",
},
},
},
Expand Down

0 comments on commit 83e8bc0

Please sign in to comment.