Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(connector): [Paypal] implement vaulting for paypal cards via zero mandates #5324

Merged
merged 32 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c1c6023
do struct changes for mandates
KiranKBR Jul 15, 2024
5f617ee
remove comments
KiranKBR Jul 15, 2024
888b533
do changes for non zero mandates
KiranKBR Jul 15, 2024
744876a
chore: run formatter
hyperswitch-bot[bot] Jul 15, 2024
19cb59c
add connector_customer_id to mandate reference
KiranKBR Jul 16, 2024
a94c6cc
add changes to MandateReference
KiranKBR Jul 17, 2024
7a6764e
Merge branch 'main' of https://github.com/juspay/hyperswitch into pay…
KiranKBR Jul 22, 2024
6020ff0
include card vault request
KiranKBR Jul 23, 2024
47c367b
Merge branch 'main' of https://github.com/juspay/hyperswitch into pay…
KiranKBR Aug 22, 2024
7a23d81
change session flow paypal metadata
KiranKBR Aug 22, 2024
4e3bb00
remove braintree graphql
KiranKBR Aug 22, 2024
0ad1603
remove connector_customer_id from mandate reference
KiranKBR Aug 22, 2024
b6bcb25
apply nightly format
KiranKBR Aug 22, 2024
e0ac6f5
resolve comments
KiranKBR Aug 23, 2024
587a104
Merge branch 'main' of https://github.com/juspay/hyperswitch into pay…
KiranKBR Aug 29, 2024
60c0a8d
reolve clippy issues
KiranKBR Aug 29, 2024
447d472
Merge branch 'main' of https://github.com/juspay/hyperswitch into pay…
KiranKBR Aug 30, 2024
b3b0316
refactor: merge main
swangi-kumari Oct 28, 2024
0f92960
Merge branch 'main' into paypal-wallet-vaulting-while-purchasing
swangi-kumari Oct 28, 2024
41f3f26
refactor: fix
swangi-kumari Oct 29, 2024
59f2b63
refactor: add router return url
swangi-kumari Oct 30, 2024
14ac8e4
refactor: make name as optional
swangi-kumari Oct 30, 2024
9383da1
Merge branch 'main' into paypal-wallet-vaulting-while-purchasing
swangi-kumari Nov 5, 2024
c334ce5
refactor: resolve clipy
swangi-kumari Nov 5, 2024
68a3aef
refactor: git merge main
swangi-kumari Nov 5, 2024
f672978
chore: run formatter
hyperswitch-bot[bot] Nov 5, 2024
d6ecf9c
Merge branch 'main' into paypal-wallet-vaulting-zero-mandates
swangi-kumari Nov 5, 2024
6149555
refactor: paypal card zero mandate
swangi-kumari Nov 18, 2024
efeec67
refactor: clean
swangi-kumari Nov 18, 2024
9cb2444
Merge branch 'main' into paypal-wallet-vaulting-zero-mandates
swangi-kumari Nov 18, 2024
e4d6c43
refactor: merge conflicts
swangi-kumari Nov 18, 2024
fb9fa78
feat: add cypress test
swangi-kumari Nov 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is card number option?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are using the same struct CardMandateRequest for request and response of Zero Mandate Payment and in response they are not sending field number

}

#[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
Loading