mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-27 03:13:56 +08:00
feat(connector): [Paypal] implement vaulting for paypal cards via zero mandates (#5324)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: swangi-kumari <swangi.12015941@lpu.in>
This commit is contained in:
@ -652,19 +652,78 @@ impl
|
||||
types::PaymentsResponseData,
|
||||
> for Paypal
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
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<RequestContent, errors::ConnectorError> {
|
||||
let connector_req = paypal::PaypalZeroMandateRequest::try_from(req)?;
|
||||
Ok(RequestContent::Json(Box::new(connector_req)))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
_req: &types::RouterData<
|
||||
api::SetupMandate,
|
||||
types::SetupMandateRequestData,
|
||||
types::PaymentsResponseData,
|
||||
>,
|
||||
_connectors: &settings::Connectors,
|
||||
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||
Err(
|
||||
errors::ConnectorError::NotImplemented("Setup Mandate flow for Paypal".to_string())
|
||||
.into(),
|
||||
)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -425,7 +425,7 @@ pub struct RedirectRequest {
|
||||
experience_context: ContextStruct,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ContextStruct {
|
||||
return_url: Option<String>,
|
||||
cancel_url: Option<String>,
|
||||
@ -433,13 +433,13 @@ pub struct ContextStruct {
|
||||
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,
|
||||
@ -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 {
|
||||
@ -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(),
|
||||
@ -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
|
||||
|
||||
@ -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: {
|
||||
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user