mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-27 11:24:45 +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,
|
types::PaymentsResponseData,
|
||||||
> for Paypal
|
> 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(
|
fn build_request(
|
||||||
&self,
|
&self,
|
||||||
_req: &types::RouterData<
|
req: &types::SetupMandateRouterData,
|
||||||
api::SetupMandate,
|
connectors: &settings::Connectors,
|
||||||
types::SetupMandateRequestData,
|
) -> CustomResult<Option<common_utils::request::Request>, errors::ConnectorError> {
|
||||||
types::PaymentsResponseData,
|
Ok(Some(
|
||||||
>,
|
services::RequestBuilder::new()
|
||||||
_connectors: &settings::Connectors,
|
.method(services::Method::Post)
|
||||||
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
.url(&types::SetupMandateType::get_url(self, req, connectors)?)
|
||||||
Err(
|
.attach_default_headers()
|
||||||
errors::ConnectorError::NotImplemented("Setup Mandate flow for Paypal".to_string())
|
.headers(types::SetupMandateType::get_headers(self, req, connectors)?)
|
||||||
.into(),
|
.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,
|
experience_context: ContextStruct,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct ContextStruct {
|
pub struct ContextStruct {
|
||||||
return_url: Option<String>,
|
return_url: Option<String>,
|
||||||
cancel_url: Option<String>,
|
cancel_url: Option<String>,
|
||||||
@ -433,13 +433,13 @@ pub struct ContextStruct {
|
|||||||
shipping_preference: ShippingPreference,
|
shipping_preference: ShippingPreference,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub enum UserAction {
|
pub enum UserAction {
|
||||||
#[serde(rename = "PAY_NOW")]
|
#[serde(rename = "PAY_NOW")]
|
||||||
PayNow,
|
PayNow,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub enum ShippingPreference {
|
pub enum ShippingPreference {
|
||||||
#[serde(rename = "SET_PROVIDED_ADDRESS")]
|
#[serde(rename = "SET_PROVIDED_ADDRESS")]
|
||||||
SetProvidedAddress,
|
SetProvidedAddress,
|
||||||
@ -527,6 +527,132 @@ pub struct PaypalPaymentsRequest {
|
|||||||
payment_source: Option<PaymentSourceItem>,
|
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> {
|
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());
|
let address = payment_address.and_then(|payment_address| payment_address.address.as_ref());
|
||||||
match address {
|
match address {
|
||||||
@ -973,11 +1099,11 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
let payment_source = match payment_method_type {
|
let payment_source = match payment_method_type {
|
||||||
enums::PaymentMethodType::Credit => Ok(Some(PaymentSourceItem::Card(
|
enums::PaymentMethodType::Credit | enums::PaymentMethodType::Debit => Ok(Some(
|
||||||
CardRequest::CardVaultStruct(VaultStruct {
|
PaymentSourceItem::Card(CardRequest::CardVaultStruct(VaultStruct {
|
||||||
vault_id: connector_mandate_id.into(),
|
vault_id: connector_mandate_id.into(),
|
||||||
}),
|
})),
|
||||||
))),
|
)),
|
||||||
enums::PaymentMethodType::Paypal => Ok(Some(PaymentSourceItem::Paypal(
|
enums::PaymentMethodType::Paypal => Ok(Some(PaymentSourceItem::Paypal(
|
||||||
PaypalRedirectionRequest::PaypalVaultStruct(VaultStruct {
|
PaypalRedirectionRequest::PaypalVaultStruct(VaultStruct {
|
||||||
vault_id: connector_mandate_id.into(),
|
vault_id: connector_mandate_id.into(),
|
||||||
@ -1009,7 +1135,6 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP
|
|||||||
| enums::PaymentMethodType::Cashapp
|
| enums::PaymentMethodType::Cashapp
|
||||||
| enums::PaymentMethodType::Dana
|
| enums::PaymentMethodType::Dana
|
||||||
| enums::PaymentMethodType::DanamonVa
|
| enums::PaymentMethodType::DanamonVa
|
||||||
| enums::PaymentMethodType::Debit
|
|
||||||
| enums::PaymentMethodType::DirectCarrierBilling
|
| enums::PaymentMethodType::DirectCarrierBilling
|
||||||
| enums::PaymentMethodType::DuitNow
|
| enums::PaymentMethodType::DuitNow
|
||||||
| enums::PaymentMethodType::Efecty
|
| enums::PaymentMethodType::Efecty
|
||||||
|
|||||||
@ -16,6 +16,23 @@ const successfulThreeDSTestCardDetails = {
|
|||||||
card_cvc: "123",
|
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 = {
|
export const connectorDetails = {
|
||||||
card_pm: {
|
card_pm: {
|
||||||
PaymentIntent: {
|
PaymentIntent: {
|
||||||
@ -222,14 +239,18 @@ export const connectorDetails = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
ZeroAuthMandate: {
|
ZeroAuthMandate: {
|
||||||
Response: {
|
Request: {
|
||||||
status: 501,
|
payment_method: "card",
|
||||||
body: {
|
payment_method_data: {
|
||||||
error: {
|
card: successfulNo3DSCardDetails,
|
||||||
type: "invalid_request",
|
|
||||||
message: "Setup Mandate flow for Paypal is not implemented",
|
|
||||||
code: "IR_00",
|
|
||||||
},
|
},
|
||||||
|
currency: "USD",
|
||||||
|
mandate_data: singleUseMandateData,
|
||||||
|
},
|
||||||
|
Response: {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
status: "succeeded",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -257,15 +278,39 @@ export const connectorDetails = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Response: {
|
Response: {
|
||||||
status: 501,
|
status: 200,
|
||||||
body: {
|
body: {
|
||||||
error: {
|
status: "succeeded",
|
||||||
type: "invalid_request",
|
setup_future_usage: "off_session",
|
||||||
message: "Setup Mandate flow for Paypal is not implemented",
|
|
||||||
code: "IR_00",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
SaveCardUseNo3DSAutoCapture: {
|
SaveCardUseNo3DSAutoCapture: {
|
||||||
Request: {
|
Request: {
|
||||||
|
|||||||
Reference in New Issue
Block a user