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::{
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(

View File

@ -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>,

View File

@ -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 {