feat(connector): [Paypal] add support for payment and refund webhooks (#2003)

Signed-off-by: chikke srujan <121822803+srujanchikke@users.noreply.github.com>
Co-authored-by: Prasunna Soppa <prasunna.soppa@juspay.in>
This commit is contained in:
chikke srujan
2023-08-29 10:53:55 +05:30
committed by GitHub
parent eaefa6e15c
commit ade27f0168
4 changed files with 228 additions and 84 deletions

View File

@ -2,8 +2,9 @@ pub mod transformers;
use std::fmt::Debug;
use base64::Engine;
use common_utils::ext_traits::ByteSliceExt;
use diesel_models::enums;
use error_stack::{IntoReport, ResultExt};
use error_stack::ResultExt;
use masking::PeekInterface;
use transformers as paypal;
@ -19,6 +20,7 @@ use crate::{
errors::{self, CustomResult},
payments,
},
db::StorageInterface,
headers,
services::{
self,
@ -28,8 +30,7 @@ use crate::{
types::{
self,
api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt},
storage::enums as storage_enums,
ErrorResponse, Response,
domain, ErrorResponse, Response,
},
utils::{self, BytesExt},
};
@ -52,24 +53,6 @@ impl api::RefundExecute for Paypal {}
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> {
match payment_method {
Some(diesel_models::enums::PaymentMethod::Wallet)
| Some(diesel_models::enums::PaymentMethod::BankRedirect) => {
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(
&self,
res: Response,
@ -454,11 +437,13 @@ impl
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
req.request
.connector_transaction_id
.clone()
.ok_or(errors::ConnectorError::MissingConnectorTransactionID)?
))
}
@ -533,24 +518,31 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
| diesel_models::enums::PaymentMethod::BankRedirect => Ok(format!(
"{}v2/checkout/orders/{}",
self.base_url(connectors),
paypal_meta.order_id
)),
_ => {
let capture_id = req
.request
req.request
.connector_transaction_id
.get_connector_transaction_id()
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?;
.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::Authorize => {
let authorize_id = paypal_meta.authorize_id.ok_or(
errors::ConnectorError::RequestEncodingFailedWithReason(
"Missing Authorize id".to_string(),
),
)?;
format!("v2/payments/authorizations/{authorize_id}",)
}
transformers::PaypalPaymentIntent::Capture => {
format!("v2/payments/captures/{}", capture_id)
let capture_id = paypal_meta.capture_id.ok_or(
errors::ConnectorError::RequestEncodingFailedWithReason(
"Missing Capture id".to_string(),
),
)?;
format!("v2/payments/captures/{capture_id}")
}
};
Ok(format!("{}{}", self.base_url(connectors), psync_url))
Ok(format!("{}{psync_url}", self.base_url(connectors)))
}
}
}
@ -676,7 +668,7 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
data: &types::PaymentsCaptureRouterData,
res: Response,
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
let response: paypal::PaymentCaptureResponse = res
let response: paypal::PaypalCaptureResponse = res
.response
.parse_struct("Paypal PaymentsCaptureResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
@ -783,11 +775,16 @@ impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsRespon
req: &types::RefundsRouterData<api::Execute>,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let id = req.request.connector_transaction_id.clone();
let paypal_meta: PaypalMeta = to_connector_meta(req.request.connector_metadata.clone())?;
let capture_id = paypal_meta.capture_id.ok_or(
errors::ConnectorError::RequestEncodingFailedWithReason(
"Missing Capture id".to_string(),
),
)?;
Ok(format!(
"{}v2/payments/captures/{}/refund",
self.base_url(connectors),
id,
capture_id,
))
}
@ -910,25 +907,76 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
#[async_trait::async_trait]
impl api::IncomingWebhook for Paypal {
async fn verify_webhook_source(
&self,
_db: &dyn StorageInterface,
_request: &api::IncomingWebhookRequestDetails<'_>,
_merchant_account: &domain::MerchantAccount,
_connector_label: &str,
_key_store: &domain::MerchantKeyStore,
_object_reference_id: api_models::webhooks::ObjectReferenceId,
) -> CustomResult<bool, errors::ConnectorError> {
Ok(false) // Verify webhook source is not implemented for Paypal it requires additional apicall this function needs to be modified once we have a way to verify webhook source
}
fn get_webhook_object_reference_id(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
let payload: paypal::PaypalWebhooksBody =
request
.body
.parse_struct("PaypalWebhooksBody")
.change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
match payload.resource {
paypal::PaypalResource::PaypalCardWebhooks(resource) => {
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
api_models::payments::PaymentIdType::ConnectorTransactionId(
resource.supplementary_data.related_ids.order_id,
),
))
}
paypal::PaypalResource::PaypalRedirectsWebhooks(resource) => {
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
api_models::payments::PaymentIdType::PaymentAttemptId(
resource
.purchase_units
.first()
.map(|unit| unit.reference_id.clone())
.ok_or(errors::ConnectorError::WebhookReferenceIdNotFound)?,
),
))
}
paypal::PaypalResource::PaypalRefundWebhooks(resource) => {
Ok(api_models::webhooks::ObjectReferenceId::RefundId(
api_models::webhooks::RefundIdType::ConnectorRefundId(resource.id),
))
}
}
}
fn get_webhook_event_type(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
Ok(api::IncomingWebhookEvent::EventNotSupported)
let payload: paypal::PaypalWebooksEventType = request
.body
.parse_struct("PaypalWebooksEventType")
.change_context(errors::ConnectorError::WebhookEventTypeNotFound)?;
Ok(api::IncomingWebhookEvent::from(payload.event_type))
}
fn get_webhook_resource_object(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
let details: paypal::PaypalWebhooksBody = request
.body
.parse_struct("PaypalWebooksEventType")
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?;
let res_json = utils::Encode::<transformers::PaypalWebhooksBody>::encode_to_value(&details)
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?;
Ok(res_json)
}
}

View File

@ -167,9 +167,10 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaypalPaymentsRequest {
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
match item.request.payment_method_data {
api_models::payments::PaymentMethodData::Card(ref ccard) => {
let intent = match item.request.is_auto_capture()? {
true => PaypalPaymentIntent::Capture,
false => PaypalPaymentIntent::Authorize,
let intent = if item.request.is_auto_capture()? {
PaypalPaymentIntent::Capture
} else {
PaypalPaymentIntent::Authorize
};
let amount = OrderAmount {
currency_code: item.request.currency,
@ -203,7 +204,13 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaypalPaymentsRequest {
}
api::PaymentMethodData::Wallet(ref wallet_data) => match wallet_data {
api_models::payments::WalletData::PaypalRedirect(_) => {
let intent = PaypalPaymentIntent::Capture;
let intent = if item.request.is_auto_capture()? {
PaypalPaymentIntent::Capture
} else {
Err(errors::ConnectorError::NotImplemented(
"Manual capture method for Paypal wallet".to_string(),
))?
};
let amount = OrderAmount {
currency_code: item.request.currency,
value: utils::to_currency_base_unit_with_zero_decimal_check(
@ -235,12 +242,13 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaypalPaymentsRequest {
))?,
},
api::PaymentMethodData::BankRedirect(ref bank_redirection_data) => {
let intent = match item.request.is_auto_capture()? {
true => PaypalPaymentIntent::Capture,
false => Err(errors::ConnectorError::FlowNotSupported {
let intent = if item.request.is_auto_capture()? {
PaypalPaymentIntent::Capture
} else {
Err(errors::ConnectorError::FlowNotSupported {
flow: "Manual capture method for Bank Redirect".to_string(),
connector: "Paypal".to_string(),
})?,
})?
};
let amount = OrderAmount {
currency_code: item.request.currency,
@ -407,12 +415,13 @@ pub struct PaypalPaymentsSyncResponse {
id: String,
status: PaypalPaymentStatus,
amount: OrderAmount,
supplementary_data: PaypalSupplementaryData,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PaypalMeta {
pub authorize_id: Option<String>,
pub order_id: String,
pub capture_id: Option<String>,
pub psync_flow: PaypalPaymentIntent,
}
@ -464,19 +473,19 @@ impl<F, T>
PaypalPaymentIntent::Capture => (
serde_json::json!(PaypalMeta {
authorize_id: None,
order_id: item.response.id,
capture_id: Some(id),
psync_flow: item.response.intent.clone()
}),
types::ResponseId::ConnectorTransactionId(id),
types::ResponseId::ConnectorTransactionId(item.response.id),
),
PaypalPaymentIntent::Authorize => (
serde_json::json!(PaypalMeta {
authorize_id: Some(id),
order_id: item.response.id,
capture_id: None,
psync_flow: item.response.intent.clone()
}),
types::ResponseId::NoResponseId,
types::ResponseId::ConnectorTransactionId(item.response.id),
),
};
//payment collection will always have only one element as we only make one transaction per order.
@ -541,14 +550,14 @@ impl<F, T>
let link = get_redirect_url(item.response.clone())?;
let connector_meta = serde_json::json!(PaypalMeta {
authorize_id: None,
order_id: item.response.id,
capture_id: None,
psync_flow: item.response.intent
});
Ok(Self {
status,
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::NoResponseId,
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),
redirection_data: Some(services::RedirectForm::from((
link.ok_or(errors::ConnectorError::ResponseDeserializationFailed)?,
services::Method::Get,
@ -580,7 +589,9 @@ impl<F, T>
Ok(Self {
status: storage_enums::AttemptStatus::from(item.response.status),
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),
resource_id: types::ResponseId::ConnectorTransactionId(
item.response.supplementary_data.related_ids.order_id,
),
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
@ -622,6 +633,7 @@ pub enum PaypalPaymentStatus {
Captured,
Completed,
Declined,
Voided,
Failed,
Pending,
Denied,
@ -631,7 +643,7 @@ pub enum PaypalPaymentStatus {
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PaymentCaptureResponse {
pub struct PaypalCaptureResponse {
id: String,
status: PaypalPaymentStatus,
amount: Option<OrderAmount>,
@ -650,16 +662,17 @@ impl From<PaypalPaymentStatus> for storage_enums::AttemptStatus {
PaypalPaymentStatus::Pending => Self::Pending,
PaypalPaymentStatus::Denied | PaypalPaymentStatus::Expired => Self::Failure,
PaypalPaymentStatus::PartiallyCaptured => Self::PartialCharged,
PaypalPaymentStatus::Voided => Self::Voided,
}
}
}
impl TryFrom<types::PaymentsCaptureResponseRouterData<PaymentCaptureResponse>>
impl TryFrom<types::PaymentsCaptureResponseRouterData<PaypalCaptureResponse>>
for types::PaymentsCaptureRouterData
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::PaymentsCaptureResponseRouterData<PaymentCaptureResponse>,
item: types::PaymentsCaptureResponseRouterData<PaypalCaptureResponse>,
) -> Result<Self, Self::Error> {
let amount_captured = item.data.request.amount_to_capture;
let status = storage_enums::AttemptStatus::from(item.response.status);
@ -668,12 +681,14 @@ impl TryFrom<types::PaymentsCaptureResponseRouterData<PaymentCaptureResponse>>
Ok(Self {
status,
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),
resource_id: types::ResponseId::ConnectorTransactionId(
item.data.request.connector_transaction_id.clone(),
),
redirection_data: None,
mandate_reference: None,
connector_metadata: Some(serde_json::json!(PaypalMeta {
authorize_id: connector_payment_id.authorize_id,
order_id: item.data.request.connector_transaction_id.clone(),
capture_id: Some(item.response.id),
psync_flow: PaypalPaymentIntent::Capture
})),
network_txn_id: None,
@ -853,3 +868,101 @@ pub struct PaypalAccessTokenErrorResponse {
pub error: String,
pub error_description: String,
}
#[derive(Deserialize, Debug, Serialize)]
pub struct PaypalWebhooksBody {
pub event_type: PaypalWebhookEventType,
pub resource: PaypalResource,
}
#[derive(Deserialize, Debug, Serialize)]
pub enum PaypalWebhookEventType {
#[serde(rename = "PAYMENT.AUTHORIZATION.CREATED")]
PaymentAuthorizationCreated,
#[serde(rename = "PAYMENT.AUTHORIZATION.VOIDED")]
PaymentAuthorizationVoided,
#[serde(rename = "PAYMENT.CAPTURE.DECLINED")]
PaymentCaptureDeclined,
#[serde(rename = "PAYMENT.CAPTURE.COMPLETED")]
PaymentCaptureCompleted,
#[serde(rename = "PAYMENT.CAPTURE.PENDING")]
PaymentCapturePending,
#[serde(rename = "PAYMENT.CAPTURE.REFUNDED")]
PaymentCaptureRefunded,
#[serde(rename = "CHECKOUT.ORDER.APPROVED")]
CheckoutOrderApproved,
#[serde(rename = "CHECKOUT.ORDER.COMPLETED")]
CheckoutOrderCompleted,
#[serde(rename = "CHECKOUT.ORDER.PROCESSED")]
CheckoutOrderProcessed,
#[serde(other)]
Unknown,
}
#[derive(Deserialize, Debug, Serialize)]
#[serde(untagged)]
pub enum PaypalResource {
PaypalCardWebhooks(Box<PaypalCardWebhooks>),
PaypalRedirectsWebhooks(Box<PaypalRedirectsWebhooks>),
PaypalRefundWebhooks(Box<PaypalRefundWebhooks>),
}
#[derive(Deserialize, Debug, Serialize)]
pub struct PaypalRefundWebhooks {
pub id: String,
pub amount: OrderAmount,
pub seller_payable_breakdown: PaypalSellerPayableBreakdown,
}
#[derive(Deserialize, Debug, Serialize)]
pub struct PaypalSellerPayableBreakdown {
pub total_refunded_amount: OrderAmount,
}
#[derive(Deserialize, Debug, Serialize)]
pub struct PaypalCardWebhooks {
pub supplementary_data: PaypalSupplementaryData,
pub amount: OrderAmount,
}
#[derive(Deserialize, Debug, Serialize)]
pub struct PaypalRedirectsWebhooks {
pub purchase_units: Vec<PaypalWebhooksPurchaseUnits>,
}
#[derive(Deserialize, Debug, Serialize)]
pub struct PaypalWebhooksPurchaseUnits {
pub reference_id: String,
pub amount: OrderAmount,
}
#[derive(Deserialize, Debug, Serialize)]
pub struct PaypalSupplementaryData {
pub related_ids: PaypalRelatedIds,
}
#[derive(Deserialize, Debug, Serialize)]
pub struct PaypalRelatedIds {
pub order_id: String,
}
#[derive(Deserialize, Debug, Serialize)]
pub struct PaypalWebooksEventType {
pub event_type: PaypalWebhookEventType,
}
impl From<PaypalWebhookEventType> for api::IncomingWebhookEvent {
fn from(event: PaypalWebhookEventType) -> Self {
match event {
PaypalWebhookEventType::PaymentCaptureCompleted
| PaypalWebhookEventType::CheckoutOrderCompleted => Self::PaymentIntentSuccess,
PaypalWebhookEventType::PaymentCapturePending
| PaypalWebhookEventType::CheckoutOrderApproved
| PaypalWebhookEventType::CheckoutOrderProcessed => Self::PaymentIntentProcessing,
PaypalWebhookEventType::PaymentCaptureDeclined => Self::PaymentIntentFailure,
PaypalWebhookEventType::PaymentCaptureRefunded => Self::RefundSuccess,
PaypalWebhookEventType::Unknown
| PaypalWebhookEventType::PaymentAuthorizationCreated
| PaypalWebhookEventType::PaymentAuthorizationVoided => Self::EventNotSupported,
}
}
}

View File

@ -352,6 +352,7 @@ default_imp_for_connector_request_id!(
connector::Opennode,
connector::Payeezy,
connector::Payme,
connector::Paypal,
connector::Payu,
connector::Powertranz,
connector::Rapyd,

View File

@ -8,7 +8,7 @@ use router_env::{instrument, tracing};
use super::{flows::Feature, PaymentAddress, PaymentData};
use crate::{
configs::settings::{ConnectorRequestReferenceIdConfig, Server},
connector::{Nexinets, Paypal},
connector::Nexinets,
core::{
errors::{self, RouterResponse, RouterResult},
payments::{self, helpers},
@ -986,24 +986,6 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsSyncData
}
}
impl api::ConnectorTransactionId for Paypal {
fn connector_transaction_id(
&self,
payment_attempt: storage::PaymentAttempt,
) -> Result<Option<String>, errors::ApiErrorResponse> {
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),
}
}
}
impl api::ConnectorTransactionId for Nexinets {
fn connector_transaction_id(
&self,