feat(connector) : add PayPal wallet support for Paypal (#893)

Co-authored-by: Arjun Karthik <m.arjunkarthik@gmail.com>
This commit is contained in:
Prasunna Soppa
2023-04-21 02:54:29 +05:30
committed by GitHub
parent e161d92c58
commit a475a76db6
4 changed files with 288 additions and 63 deletions

View File

@ -19,6 +19,7 @@ use crate::{
types::{ types::{
self, self,
api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt}, api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt},
storage::enums as storage_enums,
ErrorResponse, Response, ErrorResponse, Response,
}, },
utils::{self, BytesExt}, utils::{self, BytesExt},
@ -44,10 +45,19 @@ impl api::RefundSync for Paypal {}
impl Paypal { impl Paypal {
pub fn connector_transaction_id( pub fn connector_transaction_id(
&self, &self,
payment_method: Option<storage_enums::PaymentMethod>,
connector_meta: &Option<serde_json::Value>, connector_meta: &Option<serde_json::Value>,
) -> CustomResult<Option<String>, errors::ConnectorError> { ) -> CustomResult<Option<String>, errors::ConnectorError> {
let meta: PaypalMeta = to_connector_meta(connector_meta.clone())?; match payment_method {
Ok(meta.authorize_id) 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( pub fn get_order_error_response(
@ -187,13 +197,11 @@ impl
types::PaymentsResponseData, types::PaymentsResponseData,
> for Paypal > for Paypal
{ {
// Not Implemented (R)
} }
impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData> impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
for Paypal for Paypal
{ {
//TODO: implement sessions flow
} }
impl ConnectorIntegration<api::AccessTokenAuth, types::AccessTokenRequestData, types::AccessToken> impl ConnectorIntegration<api::AccessTokenAuth, types::AccessTokenRequestData, types::AccessToken>
@ -317,7 +325,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
_req: &types::PaymentsAuthorizeRouterData, _req: &types::PaymentsAuthorizeRouterData,
connectors: &settings::Connectors, connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> { ) -> 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( fn get_request_body(
@ -355,15 +363,30 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
data: &types::PaymentsAuthorizeRouterData, data: &types::PaymentsAuthorizeRouterData,
res: Response, res: Response,
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> { ) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
let response: paypal::PaypalOrdersResponse = res match data.payment_method {
.response storage_models::enums::PaymentMethod::Wallet => {
.parse_struct("Paypal PaymentsAuthorizeResponse") let response: paypal::PaypalRedirectResponse = res
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?; .response
types::RouterData::try_from(types::ResponseRouterData { .parse_struct("paypal PaymentsRedirectResponse")
response, .change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
data: data.clone(), types::RouterData::try_from(types::ResponseRouterData {
http_code: res.status_code, 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( fn get_error_response(
@ -381,6 +404,74 @@ impl
types::PaymentsResponseData, types::PaymentsResponseData,
> for Paypal > 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> impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
@ -403,22 +494,31 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
req: &types::PaymentsSyncRouterData, req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors, connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> { ) -> 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 paypal_meta: PaypalMeta = to_connector_meta(req.request.connector_meta.clone())?;
let psync_url = match paypal_meta.psync_flow { match req.payment_method {
transformers::PaypalPaymentIntent::Authorize => format!( storage_models::enums::PaymentMethod::Wallet => Ok(format!(
"v2/payments/authorizations/{}", "{}v2/checkout/orders/{}",
paypal_meta.authorize_id.unwrap_or_default() self.base_url(connectors),
), paypal_meta.order_id
transformers::PaypalPaymentIntent::Capture => { )),
format!("v2/payments/captures/{}", capture_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( fn build_request(
@ -440,15 +540,30 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
data: &types::PaymentsSyncRouterData, data: &types::PaymentsSyncRouterData,
res: Response, res: Response,
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> { ) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
let response: paypal::PaypalPaymentsSyncResponse = res match data.payment_method {
.response storage_models::enums::PaymentMethod::Wallet => {
.parse_struct("paypal PaymentsSyncResponse") let response: paypal::PaypalOrdersResponse = res
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?; .response
types::RouterData::try_from(types::ResponseRouterData { .parse_struct("paypal PaymentsOrderResponse")
response, .change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
data: data.clone(), types::RouterData::try_from(types::ResponseRouterData {
http_code: res.status_code, 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( fn get_error_response(

View File

@ -1,6 +1,7 @@
use common_utils::errors::CustomResult; use common_utils::errors::CustomResult;
use masking::Secret; use masking::Secret;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url;
use crate::{ use crate::{
connector::utils::{ connector::utils::{
@ -8,7 +9,7 @@ use crate::{
PaymentsAuthorizeRequestData, PaymentsAuthorizeRequestData,
}, },
core::errors, core::errors,
pii, pii, services,
types::{self, api, storage::enums as storage_enums, transformers::ForeignFrom}, types::{self, api, storage::enums as storage_enums, transformers::ForeignFrom},
}; };
@ -47,10 +48,28 @@ pub struct CardRequest {
security_code: Option<Secret<String>>, 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)] #[derive(Debug, Serialize)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum PaymentSourceItem { pub enum PaymentSourceItem {
Card(CardRequest), Card(CardRequest),
Paypal(PaypalRedirectionRequest),
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@ -111,6 +130,35 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaypalPaymentsRequest {
payment_source, 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()), _ => 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::Voided => Self::Voided,
PaypalOrderStatus::Created | PaypalOrderStatus::Saved | PaypalOrderStatus::Approved => { PaypalOrderStatus::Created | PaypalOrderStatus::Saved => Self::Pending,
Self::Pending PaypalOrderStatus::Approved => Self::AuthenticationSuccessful,
} PaypalOrderStatus::PayerActionRequired => Self::AuthenticationPending,
PaypalOrderStatus::PayerActionRequired => Self::Authorizing,
} }
} }
} }
@ -235,6 +282,20 @@ pub struct PaypalOrdersResponse {
purchase_units: Vec<PurchaseUnitItem>, 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)] #[derive(Debug, Serialize, Deserialize)]
pub struct PaypalPaymentsSyncResponse { pub struct PaypalPaymentsSyncResponse {
id: String, id: String,
@ -312,11 +373,13 @@ impl<F, T>
types::ResponseId::NoResponseId, types::ResponseId::NoResponseId,
), ),
}; };
let status = storage_enums::AttemptStatus::foreign_from((
item.response.status,
item.response.intent,
));
Ok(Self { Ok(Self {
status: storage_enums::AttemptStatus::foreign_from(( status,
item.response.status,
item.response.intent,
)),
response: Ok(types::PaymentsResponseData::TransactionResponse { response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: capture_id, resource_id: capture_id,
redirection_data: None, 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> impl<F, T>
TryFrom< TryFrom<
types::ResponseRouterData<F, PaypalPaymentsSyncResponse, T, types::PaymentsResponseData>, types::ResponseRouterData<F, PaypalPaymentsSyncResponse, T, types::PaymentsResponseData>,

View File

@ -568,7 +568,12 @@ impl api::ConnectorTransactionId for Paypal {
&self, &self,
payment_attempt: storage::PaymentAttempt, payment_attempt: storage::PaymentAttempt,
) -> Result<Option<String>, errors::ApiErrorResponse> { ) -> 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 { match metadata {
Ok(data) => Ok(data), Ok(data) => Ok(data),
_ => Err(errors::ApiErrorResponse::ResourceIdNotFound), _ => Err(errors::ApiErrorResponse::ResourceIdNotFound),
@ -577,7 +582,7 @@ impl api::ConnectorTransactionId for Paypal {
} }
impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsCaptureData { 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> { fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result<Self, Self::Error> {
let payment_data = additional_data.payment_data; 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.state.conf.connectors,
&additional_data.connector_name, &additional_data.connector_name,
api::GetToken::Connector, api::GetToken::Connector,
); )?;
let connectors = match connector {
Ok(conn) => *conn.connector,
_ => Err(errors::ApiErrorResponse::ResourceIdNotFound)?,
};
let amount_to_capture: i64 = payment_data let amount_to_capture: i64 = payment_data
.payment_attempt .payment_attempt
.amount_to_capture .amount_to_capture
@ -598,7 +598,8 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsCaptureD
Ok(Self { Ok(Self {
amount_to_capture, amount_to_capture,
currency: payment_data.currency, currency: payment_data.currency,
connector_transaction_id: connectors connector_transaction_id: connector
.connector
.connector_transaction_id(payment_data.payment_attempt.clone())? .connector_transaction_id(payment_data.payment_attempt.clone())?
.ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?, .ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?,
payment_amount: payment_data.amount.into(), 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 { 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> { fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result<Self, Self::Error> {
let payment_data = additional_data.payment_data; 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.state.conf.connectors,
&additional_data.connector_name, &additional_data.connector_name,
api::GetToken::Connector, api::GetToken::Connector,
); )?;
let connectors = match connector {
Ok(conn) => *conn.connector,
_ => Err(errors::ApiErrorResponse::ResourceIdNotFound)?,
};
Ok(Self { Ok(Self {
amount: Some(payment_data.amount.into()), amount: Some(payment_data.amount.into()),
currency: Some(payment_data.currency), currency: Some(payment_data.currency),
connector_transaction_id: connectors connector_transaction_id: connector
.connector
.connector_transaction_id(payment_data.payment_attempt.clone())? .connector_transaction_id(payment_data.payment_attempt.clone())?
.ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?, .ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?,
cancellation_reason: payment_data.payment_attempt.cancellation_reason, 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 let browser_info: Option<types::BrowserInformation> = payment_data
.payment_attempt .payment_attempt
.browser_info .browser_info
.clone()
.map(|b| b.parse_value("BrowserInformation")) .map(|b| b.parse_value("BrowserInformation"))
.transpose() .transpose()
.change_context(errors::ApiErrorResponse::InvalidDataValue { .change_context(errors::ApiErrorResponse::InvalidDataValue {

View File

@ -4,7 +4,7 @@ function find_prev_connector() {
git checkout $self git checkout $self
cp $self $self.tmp cp $self $self.tmp
# add new connector to existing list and sort it # add new connector to existing list and sort it
connectors=(aci adyen airwallex applepay authorizedotnet bambora bluesnap braintree checkout coinbase cybersource dlocal fiserv forte globalpay klarna mollie multisafepay nexinets nuvei opennode payeezy payu rapyd shift4 stripe trustpay worldline worldpay "$1") connectors=(aci adyen airwallex applepay authorizedotnet bambora bluesnap braintree checkout coinbase cybersource dlocal fiserv forte globalpay klarna mollie multisafepay nexinets nuvei opennode paypal payeezy payu rapyd shift4 stripe trustpay worldline worldpay "$1")
IFS=$'\n' sorted=($(sort <<<"${connectors[*]}")); unset IFS IFS=$'\n' sorted=($(sort <<<"${connectors[*]}")); unset IFS
res=`echo ${sorted[@]}` res=`echo ${sorted[@]}`
sed -i'' -e "s/^ connectors=.*/ connectors=($res \"\$1\")/" $self.tmp sed -i'' -e "s/^ connectors=.*/ connectors=($res \"\$1\")/" $self.tmp