mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 01:27:31 +08:00
feat(connector) : add PayPal wallet support for Paypal (#893)
Co-authored-by: Arjun Karthik <m.arjunkarthik@gmail.com>
This commit is contained in:
@ -19,6 +19,7 @@ use crate::{
|
||||
types::{
|
||||
self,
|
||||
api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt},
|
||||
storage::enums as storage_enums,
|
||||
ErrorResponse, Response,
|
||||
},
|
||||
utils::{self, BytesExt},
|
||||
@ -44,10 +45,19 @@ impl api::RefundSync for Paypal {}
|
||||
impl Paypal {
|
||||
pub fn connector_transaction_id(
|
||||
&self,
|
||||
payment_method: Option<storage_enums::PaymentMethod>,
|
||||
connector_meta: &Option<serde_json::Value>,
|
||||
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||
let meta: PaypalMeta = to_connector_meta(connector_meta.clone())?;
|
||||
Ok(meta.authorize_id)
|
||||
match payment_method {
|
||||
Some(storage_models::enums::PaymentMethod::Wallet) => {
|
||||
let meta: PaypalMeta = to_connector_meta(connector_meta.clone())?;
|
||||
Ok(Some(meta.order_id))
|
||||
}
|
||||
_ => {
|
||||
let meta: PaypalMeta = to_connector_meta(connector_meta.clone())?;
|
||||
Ok(meta.authorize_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_order_error_response(
|
||||
@ -187,13 +197,11 @@ impl
|
||||
types::PaymentsResponseData,
|
||||
> for Paypal
|
||||
{
|
||||
// Not Implemented (R)
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
|
||||
for Paypal
|
||||
{
|
||||
//TODO: implement sessions flow
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::AccessTokenAuth, types::AccessTokenRequestData, types::AccessToken>
|
||||
@ -317,7 +325,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
|
||||
_req: &types::PaymentsAuthorizeRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
Ok(format!("{}v2/checkout/orders", self.base_url(connectors),))
|
||||
Ok(format!("{}v2/checkout/orders", self.base_url(connectors)))
|
||||
}
|
||||
|
||||
fn get_request_body(
|
||||
@ -355,15 +363,30 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
|
||||
data: &types::PaymentsAuthorizeRouterData,
|
||||
res: Response,
|
||||
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
|
||||
let response: paypal::PaypalOrdersResponse = res
|
||||
.response
|
||||
.parse_struct("Paypal PaymentsAuthorizeResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
match data.payment_method {
|
||||
storage_models::enums::PaymentMethod::Wallet => {
|
||||
let response: paypal::PaypalRedirectResponse = res
|
||||
.response
|
||||
.parse_struct("paypal PaymentsRedirectResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
let response: paypal::PaypalOrdersResponse = res
|
||||
.response
|
||||
.parse_struct("paypal PaymentsOrderResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
@ -381,6 +404,74 @@ impl
|
||||
types::PaymentsResponseData,
|
||||
> for Paypal
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::PaymentsCompleteAuthorizeRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, 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::PaymentsCompleteAuthorizeRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
let paypal_meta: PaypalMeta = to_connector_meta(req.request.connector_meta.clone())?;
|
||||
Ok(format!(
|
||||
"{}v2/checkout/orders/{}/capture",
|
||||
self.base_url(connectors),
|
||||
paypal_meta.order_id
|
||||
))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::PaymentsCompleteAuthorizeRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||
Ok(Some(
|
||||
services::RequestBuilder::new()
|
||||
.method(services::Method::Post)
|
||||
.url(&types::PaymentsCompleteAuthorizeType::get_url(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.headers(types::PaymentsCompleteAuthorizeType::get_headers(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.body(types::PaymentsCompleteAuthorizeType::get_request_body(
|
||||
self, req,
|
||||
)?)
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::PaymentsCompleteAuthorizeRouterData,
|
||||
res: Response,
|
||||
) -> CustomResult<types::PaymentsCompleteAuthorizeRouterData, errors::ConnectorError> {
|
||||
let response: paypal::PaypalOrdersResponse = res
|
||||
.response
|
||||
.parse_struct("paypal PaymentsOrderResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
|
||||
@ -403,22 +494,31 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
|
||||
req: &types::PaymentsSyncRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
let capture_id = req
|
||||
.request
|
||||
.connector_transaction_id
|
||||
.get_connector_transaction_id()
|
||||
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?;
|
||||
let paypal_meta: PaypalMeta = to_connector_meta(req.request.connector_meta.clone())?;
|
||||
let psync_url = match paypal_meta.psync_flow {
|
||||
transformers::PaypalPaymentIntent::Authorize => format!(
|
||||
"v2/payments/authorizations/{}",
|
||||
paypal_meta.authorize_id.unwrap_or_default()
|
||||
),
|
||||
transformers::PaypalPaymentIntent::Capture => {
|
||||
format!("v2/payments/captures/{}", capture_id)
|
||||
match req.payment_method {
|
||||
storage_models::enums::PaymentMethod::Wallet => Ok(format!(
|
||||
"{}v2/checkout/orders/{}",
|
||||
self.base_url(connectors),
|
||||
paypal_meta.order_id
|
||||
)),
|
||||
_ => {
|
||||
let capture_id = req
|
||||
.request
|
||||
.connector_transaction_id
|
||||
.get_connector_transaction_id()
|
||||
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?;
|
||||
let psync_url = match paypal_meta.psync_flow {
|
||||
transformers::PaypalPaymentIntent::Authorize => format!(
|
||||
"v2/payments/authorizations/{}",
|
||||
paypal_meta.authorize_id.unwrap_or_default()
|
||||
),
|
||||
transformers::PaypalPaymentIntent::Capture => {
|
||||
format!("v2/payments/captures/{}", capture_id)
|
||||
}
|
||||
};
|
||||
Ok(format!("{}{}", self.base_url(connectors), psync_url))
|
||||
}
|
||||
};
|
||||
Ok(format!("{}{}", self.base_url(connectors), psync_url,))
|
||||
}
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
@ -440,15 +540,30 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
|
||||
data: &types::PaymentsSyncRouterData,
|
||||
res: Response,
|
||||
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
|
||||
let response: paypal::PaypalPaymentsSyncResponse = res
|
||||
.response
|
||||
.parse_struct("paypal PaymentsSyncResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
match data.payment_method {
|
||||
storage_models::enums::PaymentMethod::Wallet => {
|
||||
let response: paypal::PaypalOrdersResponse = res
|
||||
.response
|
||||
.parse_struct("paypal PaymentsOrderResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
let response: paypal::PaypalPaymentsSyncResponse = res
|
||||
.response
|
||||
.parse_struct("paypal PaymentsSyncResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use common_utils::errors::CustomResult;
|
||||
use masking::Secret;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
connector::utils::{
|
||||
@ -8,7 +9,7 @@ use crate::{
|
||||
PaymentsAuthorizeRequestData,
|
||||
},
|
||||
core::errors,
|
||||
pii,
|
||||
pii, services,
|
||||
types::{self, api, storage::enums as storage_enums, transformers::ForeignFrom},
|
||||
};
|
||||
|
||||
@ -47,10 +48,28 @@ pub struct CardRequest {
|
||||
security_code: Option<Secret<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct RedirectRequest {
|
||||
name: Secret<String>,
|
||||
country_code: api_models::enums::CountryCode,
|
||||
experience_context: ContextStruct,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ContextStruct {
|
||||
return_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PaypalRedirectionRequest {
|
||||
experience_context: ContextStruct,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PaymentSourceItem {
|
||||
Card(CardRequest),
|
||||
Paypal(PaypalRedirectionRequest),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@ -111,6 +130,35 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaypalPaymentsRequest {
|
||||
payment_source,
|
||||
})
|
||||
}
|
||||
api::PaymentMethodData::Wallet(ref wallet_data) => match wallet_data {
|
||||
api_models::payments::WalletData::PaypalRedirect(_) => {
|
||||
let intent = PaypalPaymentIntent::Capture;
|
||||
let amount = OrderAmount {
|
||||
currency_code: item.request.currency,
|
||||
value: item.request.amount.to_string(),
|
||||
};
|
||||
let reference_id = item.attempt_id.clone();
|
||||
let purchase_units = vec![PurchaseUnitRequest {
|
||||
reference_id,
|
||||
amount,
|
||||
}];
|
||||
let payment_source =
|
||||
Some(PaymentSourceItem::Paypal(PaypalRedirectionRequest {
|
||||
experience_context: ContextStruct {
|
||||
return_url: item.request.complete_authorize_url.clone(),
|
||||
},
|
||||
}));
|
||||
|
||||
Ok(Self {
|
||||
intent,
|
||||
purchase_units,
|
||||
payment_source,
|
||||
})
|
||||
}
|
||||
_ => Err(errors::ConnectorError::NotImplemented(
|
||||
"Payment Method".to_string(),
|
||||
))?,
|
||||
},
|
||||
_ => Err(errors::ConnectorError::NotImplemented("Payment Method".to_string()).into()),
|
||||
}
|
||||
}
|
||||
@ -198,10 +246,9 @@ impl ForeignFrom<(PaypalOrderStatus, PaypalPaymentIntent)> for storage_enums::At
|
||||
}
|
||||
}
|
||||
PaypalOrderStatus::Voided => Self::Voided,
|
||||
PaypalOrderStatus::Created | PaypalOrderStatus::Saved | PaypalOrderStatus::Approved => {
|
||||
Self::Pending
|
||||
}
|
||||
PaypalOrderStatus::PayerActionRequired => Self::Authorizing,
|
||||
PaypalOrderStatus::Created | PaypalOrderStatus::Saved => Self::Pending,
|
||||
PaypalOrderStatus::Approved => Self::AuthenticationSuccessful,
|
||||
PaypalOrderStatus::PayerActionRequired => Self::AuthenticationPending,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -235,6 +282,20 @@ pub struct PaypalOrdersResponse {
|
||||
purchase_units: Vec<PurchaseUnitItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PaypalLinks {
|
||||
href: Option<Url>,
|
||||
rel: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PaypalRedirectResponse {
|
||||
id: String,
|
||||
intent: PaypalPaymentIntent,
|
||||
status: PaypalOrderStatus,
|
||||
links: Vec<PaypalLinks>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PaypalPaymentsSyncResponse {
|
||||
id: String,
|
||||
@ -312,11 +373,13 @@ impl<F, T>
|
||||
types::ResponseId::NoResponseId,
|
||||
),
|
||||
};
|
||||
let status = storage_enums::AttemptStatus::foreign_from((
|
||||
item.response.status,
|
||||
item.response.intent,
|
||||
));
|
||||
|
||||
Ok(Self {
|
||||
status: storage_enums::AttemptStatus::foreign_from((
|
||||
item.response.status,
|
||||
item.response.intent,
|
||||
)),
|
||||
status,
|
||||
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: capture_id,
|
||||
redirection_data: None,
|
||||
@ -328,6 +391,54 @@ impl<F, T>
|
||||
}
|
||||
}
|
||||
|
||||
fn get_redirect_url(
|
||||
item: PaypalRedirectResponse,
|
||||
) -> CustomResult<Option<Url>, errors::ConnectorError> {
|
||||
let mut link: Option<Url> = None;
|
||||
let link_vec = item.links;
|
||||
for item2 in link_vec.iter() {
|
||||
if item2.rel == "payer-action" {
|
||||
link = item2.href.clone();
|
||||
}
|
||||
}
|
||||
Ok(link)
|
||||
}
|
||||
|
||||
impl<F, T>
|
||||
TryFrom<types::ResponseRouterData<F, PaypalRedirectResponse, T, types::PaymentsResponseData>>
|
||||
for types::RouterData<F, T, types::PaymentsResponseData>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::ResponseRouterData<F, PaypalRedirectResponse, T, types::PaymentsResponseData>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let status = storage_enums::AttemptStatus::foreign_from((
|
||||
item.response.clone().status,
|
||||
item.response.intent.clone(),
|
||||
));
|
||||
let link = get_redirect_url(item.response.clone())?;
|
||||
let connector_meta = serde_json::json!(PaypalMeta {
|
||||
authorize_id: None,
|
||||
order_id: item.response.id,
|
||||
psync_flow: item.response.intent
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
status,
|
||||
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: types::ResponseId::NoResponseId,
|
||||
redirection_data: Some(services::RedirectForm::from((
|
||||
link.ok_or(errors::ConnectorError::ResponseDeserializationFailed)?,
|
||||
services::Method::Get,
|
||||
))),
|
||||
mandate_reference: None,
|
||||
connector_metadata: Some(connector_meta),
|
||||
}),
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, T>
|
||||
TryFrom<
|
||||
types::ResponseRouterData<F, PaypalPaymentsSyncResponse, T, types::PaymentsResponseData>,
|
||||
|
||||
@ -568,7 +568,12 @@ impl api::ConnectorTransactionId for Paypal {
|
||||
&self,
|
||||
payment_attempt: storage::PaymentAttempt,
|
||||
) -> Result<Option<String>, errors::ApiErrorResponse> {
|
||||
let metadata = Self::connector_transaction_id(self, &payment_attempt.connector_metadata);
|
||||
let payment_method = payment_attempt.payment_method;
|
||||
let metadata = Self::connector_transaction_id(
|
||||
self,
|
||||
payment_method,
|
||||
&payment_attempt.connector_metadata,
|
||||
);
|
||||
match metadata {
|
||||
Ok(data) => Ok(data),
|
||||
_ => Err(errors::ApiErrorResponse::ResourceIdNotFound),
|
||||
@ -577,7 +582,7 @@ impl api::ConnectorTransactionId for Paypal {
|
||||
}
|
||||
|
||||
impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsCaptureData {
|
||||
type Error = errors::ApiErrorResponse;
|
||||
type Error = error_stack::Report<errors::ApiErrorResponse>;
|
||||
|
||||
fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result<Self, Self::Error> {
|
||||
let payment_data = additional_data.payment_data;
|
||||
@ -585,12 +590,7 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsCaptureD
|
||||
&additional_data.state.conf.connectors,
|
||||
&additional_data.connector_name,
|
||||
api::GetToken::Connector,
|
||||
);
|
||||
let connectors = match connector {
|
||||
Ok(conn) => *conn.connector,
|
||||
_ => Err(errors::ApiErrorResponse::ResourceIdNotFound)?,
|
||||
};
|
||||
|
||||
)?;
|
||||
let amount_to_capture: i64 = payment_data
|
||||
.payment_attempt
|
||||
.amount_to_capture
|
||||
@ -598,7 +598,8 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsCaptureD
|
||||
Ok(Self {
|
||||
amount_to_capture,
|
||||
currency: payment_data.currency,
|
||||
connector_transaction_id: connectors
|
||||
connector_transaction_id: connector
|
||||
.connector
|
||||
.connector_transaction_id(payment_data.payment_attempt.clone())?
|
||||
.ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?,
|
||||
payment_amount: payment_data.amount.into(),
|
||||
@ -608,7 +609,7 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsCaptureD
|
||||
}
|
||||
|
||||
impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsCancelData {
|
||||
type Error = errors::ApiErrorResponse;
|
||||
type Error = error_stack::Report<errors::ApiErrorResponse>;
|
||||
|
||||
fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result<Self, Self::Error> {
|
||||
let payment_data = additional_data.payment_data;
|
||||
@ -616,15 +617,12 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsCancelDa
|
||||
&additional_data.state.conf.connectors,
|
||||
&additional_data.connector_name,
|
||||
api::GetToken::Connector,
|
||||
);
|
||||
let connectors = match connector {
|
||||
Ok(conn) => *conn.connector,
|
||||
_ => Err(errors::ApiErrorResponse::ResourceIdNotFound)?,
|
||||
};
|
||||
)?;
|
||||
Ok(Self {
|
||||
amount: Some(payment_data.amount.into()),
|
||||
currency: Some(payment_data.currency),
|
||||
connector_transaction_id: connectors
|
||||
connector_transaction_id: connector
|
||||
.connector
|
||||
.connector_transaction_id(payment_data.payment_attempt.clone())?
|
||||
.ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?,
|
||||
cancellation_reason: payment_data.payment_attempt.cancellation_reason,
|
||||
@ -693,6 +691,7 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::CompleteAuthoriz
|
||||
let browser_info: Option<types::BrowserInformation> = payment_data
|
||||
.payment_attempt
|
||||
.browser_info
|
||||
.clone()
|
||||
.map(|b| b.parse_value("BrowserInformation"))
|
||||
.transpose()
|
||||
.change_context(errors::ApiErrorResponse::InvalidDataValue {
|
||||
|
||||
Reference in New Issue
Block a user