From c39ecda79a9b1df1ccb4e469111e0dfb92a3d82c Mon Sep 17 00:00:00 2001 From: Sagnik Mitra <83326850+ImSagnik007@users.noreply.github.com> Date: Mon, 17 Mar 2025 12:17:47 +0530 Subject: [PATCH] feat(connector): [PAYSTACK] Electronic Fund Transfer(EFT) Payment Flows (#7440) Co-authored-by: Anurag Singh Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../connector_configs/toml/development.toml | 8 + crates/connector_configs/toml/production.toml | 8 + crates/connector_configs/toml/sandbox.toml | 8 + .../src/connectors/paystack.rs | 138 ++++-- .../src/connectors/paystack/transformers.rs | 457 +++++++++++++----- 5 files changed, 482 insertions(+), 137 deletions(-) diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index 6ea07ebfa0..a1dd0cac47 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -2846,6 +2846,14 @@ client_id="Client ID" api_key="Client Secret" key1="Client ID" +[paystack] +[[paystack.bank_redirect]] + payment_method_type = "eft" +[paystack.connector_auth.HeaderKey] +api_key="API Key" +[paystack.connector_webhook_details] +merchant_secret="API Key" + [payu] [[payu.credit]] payment_method_type = "Mastercard" diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index a1cdc196d4..aada50faa6 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -2123,6 +2123,14 @@ merchant_secret="Source verification key" [paypal.metadata.paypal_sdk] client_id="Client ID" +[paystack] +[[paystack.bank_redirect]] + payment_method_type = "eft" +[paystack.connector_auth.HeaderKey] +api_key="API Key" +[paystack.connector_webhook_details] +merchant_secret="API Key" + [payu] [[payu.credit]] payment_method_type = "Mastercard" diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index c729878abd..6bf33b902f 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -3955,6 +3955,14 @@ api_key="Api Key" [paypal_test.connector_auth.HeaderKey] api_key="Api Key" +[paystack] +[[paystack.bank_redirect]] + payment_method_type = "eft" +[paystack.connector_auth.HeaderKey] +api_key="API Key" +[paystack.connector_webhook_details] +merchant_secret="API Key" + [stripe_test] [[stripe_test.credit]] payment_method_type = "Mastercard" diff --git a/crates/hyperswitch_connectors/src/connectors/paystack.rs b/crates/hyperswitch_connectors/src/connectors/paystack.rs index a88b60cc21..6d6ce2ff83 100644 --- a/crates/hyperswitch_connectors/src/connectors/paystack.rs +++ b/crates/hyperswitch_connectors/src/connectors/paystack.rs @@ -1,12 +1,13 @@ pub mod transformers; use common_utils::{ + crypto, errors::CustomResult, - ext_traits::BytesExt, + ext_traits::{ByteSliceExt, BytesExt}, request::{Method, Request, RequestBuilder, RequestContent}, - types::{AmountConvertor, StringMinorUnit, StringMinorUnitForConnector}, + types::{AmountConvertor, MinorUnit, MinorUnitForConnector}, }; -use error_stack::{report, ResultExt}; +use error_stack::ResultExt; use hyperswitch_domain_models::{ router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, router_flow_types::{ @@ -43,13 +44,13 @@ use crate::{constants::headers, types::ResponseRouterData, utils}; #[derive(Clone)] pub struct Paystack { - amount_converter: &'static (dyn AmountConvertor + Sync), + amount_converter: &'static (dyn AmountConvertor + Sync), } impl Paystack { pub fn new() -> &'static Self { &Self { - amount_converter: &StringMinorUnitForConnector, + amount_converter: &MinorUnitForConnector, } } } @@ -117,7 +118,7 @@ impl ConnectorCommon for Paystack { .change_context(errors::ConnectorError::FailedToObtainAuthType)?; Ok(vec![( headers::AUTHORIZATION.to_string(), - auth.api_key.expose().into_masked(), + format!("Bearer {}", auth.api_key.expose()).into_masked(), )]) } @@ -133,12 +134,13 @@ impl ConnectorCommon for Paystack { event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); + let error_message = paystack::get_error_message(response.clone()); Ok(ErrorResponse { status_code: res.status_code, code: response.code, - message: response.message, - reason: response.reason, + message: error_message, + reason: Some(response.message), attempt_status: None, connector_transaction_id: None, }) @@ -176,9 +178,9 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}/charge", self.base_url(connectors))) } fn get_request_body( @@ -262,10 +264,20 @@ impl ConnectorIntegration for Pay fn get_url( &self, - _req: &PaymentsSyncRouterData, - _connectors: &Connectors, + req: &PaymentsSyncRouterData, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let connector_payment_id = req + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + Ok(format!( + "{}{}{}", + self.base_url(connectors), + "/transaction/verify/", + connector_payment_id, + )) } fn build_request( @@ -289,7 +301,7 @@ impl ConnectorIntegration for Pay event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult { - let response: paystack::PaystackPaymentsResponse = res + let response: paystack::PaystackPSyncResponse = res .response .parse_struct("paystack PaymentsSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -406,9 +418,9 @@ impl ConnectorIntegration for Paystac fn get_url( &self, _req: &RefundsRouterData, - _connectors: &Connectors, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}/refund", self.base_url(connectors))) } fn get_request_body( @@ -452,7 +464,7 @@ impl ConnectorIntegration for Paystac event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult, errors::ConnectorError> { - let response: paystack::RefundResponse = res + let response: paystack::PaystackRefundsResponse = res .response .parse_struct("paystack RefundResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -489,10 +501,20 @@ impl ConnectorIntegration for Paystack fn get_url( &self, - _req: &RefundSyncRouterData, - _connectors: &Connectors, + req: &RefundSyncRouterData, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let connector_refund_id = req + .request + .connector_refund_id + .clone() + .ok_or(errors::ConnectorError::MissingConnectorRefundID)?; + Ok(format!( + "{}{}{}", + self.base_url(connectors), + "/refund/", + connector_refund_id, + )) } fn build_request( @@ -519,7 +541,7 @@ impl ConnectorIntegration for Paystack event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult { - let response: paystack::RefundResponse = res + let response: paystack::PaystackRefundsResponse = res .response .parse_struct("paystack RefundSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -543,25 +565,87 @@ impl ConnectorIntegration for Paystack #[async_trait::async_trait] impl webhooks::IncomingWebhook for Paystack { - fn get_webhook_object_reference_id( + fn get_webhook_source_verification_algorithm( &self, _request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + Ok(Box::new(crypto::HmacSha512)) + } + + fn get_webhook_source_verification_signature( + &self, + request: &webhooks::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + ) -> CustomResult, errors::ConnectorError> { + let signature = utils::get_header_key_value("x-paystack-signature", request.headers) + .change_context(errors::ConnectorError::WebhookSignatureNotFound)?; + + hex::decode(signature) + .change_context(errors::ConnectorError::WebhookVerificationSecretInvalid) + } + + fn get_webhook_source_verification_message( + &self, + request: &webhooks::IncomingWebhookRequestDetails<'_>, + _merchant_id: &common_utils::id_type::MerchantId, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + ) -> CustomResult, errors::ConnectorError> { + let message = std::str::from_utf8(request.body) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + Ok(message.to_string().into_bytes()) + } + + fn get_webhook_object_reference_id( + &self, + request: &webhooks::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + let webhook_body = request + .body + .parse_struct::("PaystackWebhookData") + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + match webhook_body.data { + paystack::PaystackWebhookEventData::Payment(data) => { + Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::ConnectorTransactionId(data.reference), + )) + } + paystack::PaystackWebhookEventData::Refund(data) => { + Ok(api_models::webhooks::ObjectReferenceId::RefundId( + api_models::webhooks::RefundIdType::ConnectorRefundId(data.id), + )) + } + } } fn get_webhook_event_type( &self, - _request: &webhooks::IncomingWebhookRequestDetails<'_>, + request: &webhooks::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + let webhook_body = request + .body + .parse_struct::("PaystackWebhookData") + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + Ok(api_models::webhooks::IncomingWebhookEvent::from( + webhook_body.data, + )) } fn get_webhook_resource_object( &self, - _request: &webhooks::IncomingWebhookRequestDetails<'_>, + request: &webhooks::IncomingWebhookRequestDetails<'_>, ) -> CustomResult, errors::ConnectorError> { - Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + let webhook_body = request + .body + .parse_struct::("PaystackWebhookData") + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + Ok(match webhook_body.data { + paystack::PaystackWebhookEventData::Payment(payment_webhook_data) => { + Box::new(payment_webhook_data) + } + paystack::PaystackWebhookEventData::Refund(refund_webhook_data) => { + Box::new(refund_webhook_data) + } + }) } } diff --git a/crates/hyperswitch_connectors/src/connectors/paystack/transformers.rs b/crates/hyperswitch_connectors/src/connectors/paystack/transformers.rs index 8a07997fbd..2ced7d44d5 100644 --- a/crates/hyperswitch_connectors/src/connectors/paystack/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/paystack/transformers.rs @@ -1,30 +1,31 @@ -use common_enums::enums; -use common_utils::types::StringMinorUnit; +use common_enums::{enums, Currency}; +use common_utils::{pii::Email, request::Method, types::MinorUnit}; +use error_stack::ResultExt; use hyperswitch_domain_models::{ - payment_method_data::PaymentMethodData, - router_data::{ConnectorAuthType, RouterData}, + payment_method_data::{BankRedirectData, PaymentMethodData}, + router_data::{ConnectorAuthType, ErrorResponse, RouterData}, router_flow_types::refunds::{Execute, RSync}, router_request_types::ResponseId, - router_response_types::{PaymentsResponseData, RefundsResponseData}, + router_response_types::{PaymentsResponseData, RedirectForm, RefundsResponseData}, types::{PaymentsAuthorizeRouterData, RefundsRouterData}, }; use hyperswitch_interfaces::errors; use masking::Secret; use serde::{Deserialize, Serialize}; +use url::Url; use crate::{ types::{RefundsResponseRouterData, ResponseRouterData}, utils::PaymentsAuthorizeRequestData, }; -//TODO: Fill the struct with respective fields pub struct PaystackRouterData { - pub amount: StringMinorUnit, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub amount: MinorUnit, pub router_data: T, } -impl From<(StringMinorUnit, T)> for PaystackRouterData { - fn from((amount, item): (StringMinorUnit, T)) -> Self { +impl From<(MinorUnit, T)> for PaystackRouterData { + fn from((amount, item): (MinorUnit, T)) -> Self { //Todo : use utils to convert the amount to the type of amount that a connector accepts Self { amount, @@ -33,20 +34,17 @@ impl From<(StringMinorUnit, T)> for PaystackRouterData { } } -//TODO: Fill the struct with respective fields #[derive(Default, Debug, Serialize, PartialEq)] -pub struct PaystackPaymentsRequest { - amount: StringMinorUnit, - card: PaystackCard, +pub struct PaystackEftProvider { + provider: String, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct PaystackCard { - number: cards::CardNumber, - expiry_month: Secret, - expiry_year: Secret, - cvc: Secret, - complete: bool, +#[derive(Default, Debug, Serialize, PartialEq)] +pub struct PaystackPaymentsRequest { + amount: MinorUnit, + currency: Currency, + email: Email, + eft: PaystackEftProvider, } impl TryFrom<&PaystackRouterData<&PaymentsAuthorizeRouterData>> for PaystackPaymentsRequest { @@ -55,17 +53,14 @@ impl TryFrom<&PaystackRouterData<&PaymentsAuthorizeRouterData>> for PaystackPaym item: &PaystackRouterData<&PaymentsAuthorizeRouterData>, ) -> Result { match item.router_data.request.payment_method_data.clone() { - PaymentMethodData::Card(req_card) => { - let card = PaystackCard { - number: req_card.card_number, - expiry_month: req_card.card_exp_month, - expiry_year: req_card.card_exp_year, - cvc: req_card.card_cvc, - complete: item.router_data.request.is_auto_capture()?, - }; + PaymentMethodData::BankRedirect(BankRedirectData::Eft { provider }) => { + let email = item.router_data.request.get_email()?; + let eft = PaystackEftProvider { provider }; Ok(Self { - amount: item.amount.clone(), - card, + amount: item.amount, + currency: item.router_data.request.currency, + email, + eft, }) } _ => Err(errors::ConnectorError::NotImplemented("Payment method".to_string()).into()), @@ -73,8 +68,6 @@ impl TryFrom<&PaystackRouterData<&PaymentsAuthorizeRouterData>> for PaystackPaym } } -//TODO: Fill the struct with respective fields -// Auth Struct pub struct PaystackAuthType { pub(super) api_key: Secret, } @@ -90,32 +83,26 @@ impl TryFrom<&ConnectorAuthType> for PaystackAuthType { } } } -// PaymentsResponse -//TODO: Append the remaining status flags -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum PaystackPaymentStatus { - Succeeded, - Failed, - #[default] - Processing, -} -impl From for common_enums::AttemptStatus { - fn from(item: PaystackPaymentStatus) -> Self { - match item { - PaystackPaymentStatus::Succeeded => Self::Charged, - PaystackPaymentStatus::Failed => Self::Failure, - PaystackPaymentStatus::Processing => Self::Authorizing, - } - } -} - -//TODO: Fill the struct with respective fields #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct PaystackPaymentsResponse { - status: PaystackPaymentStatus, - id: String, +pub struct PaystackEftRedirect { + reference: String, + status: String, + url: String, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct PaystackPaymentsResponseData { + status: bool, + message: String, + data: PaystackEftRedirect, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(untagged)] +pub enum PaystackPaymentsResponse { + PaystackPaymentsData(PaystackPaymentsResponseData), + PaystackPaymentsError(PaystackErrorResponse), } impl TryFrom> @@ -125,104 +112,354 @@ impl TryFrom, ) -> Result { + let (status, response) = match item.response { + PaystackPaymentsResponse::PaystackPaymentsData(resp) => { + let redirection_url = Url::parse(resp.data.url.as_str()) + .change_context(errors::ConnectorError::ParsingFailed)?; + let redirection_data = RedirectForm::from((redirection_url, Method::Get)); + ( + common_enums::AttemptStatus::AuthenticationPending, + Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + resp.data.reference.clone(), + ), + redirection_data: Box::new(Some(redirection_data)), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charges: None, + }), + ) + } + PaystackPaymentsResponse::PaystackPaymentsError(err) => { + let err_msg = get_error_message(err.clone()); + ( + common_enums::AttemptStatus::Failure, + Err(ErrorResponse { + code: err.code, + message: err_msg.clone(), + reason: Some(err_msg.clone()), + attempt_status: None, + connector_transaction_id: None, + status_code: item.http_code, + }), + ) + } + }; Ok(Self { - status: common_enums::AttemptStatus::from(item.response.status), - response: Ok(PaymentsResponseData::TransactionResponse { - resource_id: ResponseId::ConnectorTransactionId(item.response.id), - redirection_data: Box::new(None), - mandate_reference: Box::new(None), - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: None, - incremental_authorization_allowed: None, - charges: None, - }), + status, + response, ..item.data }) } } -//TODO: Fill the struct with respective fields -// REFUND : -// Type definition for RefundRequest -#[derive(Default, Debug, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum PaystackPSyncStatus { + Abandoned, + Failed, + Ongoing, + Pending, + Processing, + Queued, + Reversed, + Success, +} + +impl From for common_enums::AttemptStatus { + fn from(item: PaystackPSyncStatus) -> Self { + match item { + PaystackPSyncStatus::Success => Self::Charged, + PaystackPSyncStatus::Abandoned => Self::AuthenticationPending, + PaystackPSyncStatus::Ongoing + | PaystackPSyncStatus::Pending + | PaystackPSyncStatus::Processing + | PaystackPSyncStatus::Queued => Self::Pending, + PaystackPSyncStatus::Failed => Self::Failure, + PaystackPSyncStatus::Reversed => Self::Voided, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct PaystackPSyncData { + status: PaystackPSyncStatus, + reference: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct PaystackPSyncResponseData { + status: bool, + message: String, + data: PaystackPSyncData, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(untagged)] +pub enum PaystackPSyncResponse { + PaystackPSyncData(PaystackPSyncResponseData), + PaystackPSyncWebhook(PaystackPaymentWebhookData), + PaystackPSyncError(PaystackErrorResponse), +} + +impl TryFrom> + for RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + match item.response { + PaystackPSyncResponse::PaystackPSyncData(resp) => Ok(Self { + status: common_enums::AttemptStatus::from(resp.data.status), + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(resp.data.reference.clone()), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charges: None, + }), + ..item.data + }), + PaystackPSyncResponse::PaystackPSyncWebhook(resp) => Ok(Self { + status: common_enums::AttemptStatus::from(resp.status), + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(resp.reference.clone()), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charges: None, + }), + ..item.data + }), + PaystackPSyncResponse::PaystackPSyncError(err) => { + let err_msg = get_error_message(err.clone()); + Ok(Self { + response: Err(ErrorResponse { + code: err.code, + message: err_msg.clone(), + reason: Some(err_msg.clone()), + attempt_status: None, + connector_transaction_id: None, + status_code: item.http_code, + }), + ..item.data + }) + } + } + } +} + +#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] pub struct PaystackRefundRequest { - pub amount: StringMinorUnit, + pub transaction: String, + pub amount: MinorUnit, } impl TryFrom<&PaystackRouterData<&RefundsRouterData>> for PaystackRefundRequest { type Error = error_stack::Report; fn try_from(item: &PaystackRouterData<&RefundsRouterData>) -> Result { Ok(Self { + transaction: item.router_data.request.connector_transaction_id.clone(), amount: item.amount.to_owned(), }) } } -// Type definition for Refund Response - -#[allow(dead_code)] -#[derive(Debug, Serialize, Default, Deserialize, Clone)] -pub enum RefundStatus { - Succeeded, +#[derive(Debug, Serialize, Default, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum PaystackRefundStatus { + Processed, Failed, #[default] Processing, + Pending, } -impl From for enums::RefundStatus { - fn from(item: RefundStatus) -> Self { +impl From for enums::RefundStatus { + fn from(item: PaystackRefundStatus) -> Self { match item { - RefundStatus::Succeeded => Self::Success, - RefundStatus::Failed => Self::Failure, - RefundStatus::Processing => Self::Pending, - //TODO: Review mapping + PaystackRefundStatus::Processed => Self::Success, + PaystackRefundStatus::Failed => Self::Failure, + PaystackRefundStatus::Processing | PaystackRefundStatus::Pending => Self::Pending, } } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct RefundResponse { - id: String, - status: RefundStatus, +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct PaystackRefundsData { + status: PaystackRefundStatus, + id: i64, } -impl TryFrom> for RefundsRouterData { +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct PaystackRefundsResponseData { + status: bool, + message: String, + data: PaystackRefundsData, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(untagged)] +pub enum PaystackRefundsResponse { + PaystackRefundsData(PaystackRefundsResponseData), + PaystackRSyncWebhook(PaystackRefundWebhookData), + PaystackRefundsError(PaystackErrorResponse), +} + +impl TryFrom> + for RefundsRouterData +{ type Error = error_stack::Report; fn try_from( - item: RefundsResponseRouterData, + item: RefundsResponseRouterData, ) -> Result { - Ok(Self { - response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + match item.response { + PaystackRefundsResponse::PaystackRefundsData(resp) => Ok(Self { + response: Ok(RefundsResponseData { + connector_refund_id: resp.data.id.to_string(), + refund_status: enums::RefundStatus::from(resp.data.status), + }), + ..item.data }), - ..item.data - }) + PaystackRefundsResponse::PaystackRSyncWebhook(resp) => Ok(Self { + response: Ok(RefundsResponseData { + connector_refund_id: resp.id, + refund_status: enums::RefundStatus::from(resp.status), + }), + ..item.data + }), + PaystackRefundsResponse::PaystackRefundsError(err) => { + let err_msg = get_error_message(err.clone()); + Ok(Self { + response: Err(ErrorResponse { + code: err.code, + message: err_msg.clone(), + reason: Some(err_msg.clone()), + attempt_status: None, + connector_transaction_id: None, + status_code: item.http_code, + }), + ..item.data + }) + } + } } } -impl TryFrom> for RefundsRouterData { +impl TryFrom> + for RefundsRouterData +{ type Error = error_stack::Report; fn try_from( - item: RefundsResponseRouterData, + item: RefundsResponseRouterData, ) -> Result { - Ok(Self { - response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + match item.response { + PaystackRefundsResponse::PaystackRefundsData(resp) => Ok(Self { + response: Ok(RefundsResponseData { + connector_refund_id: resp.data.id.to_string(), + refund_status: enums::RefundStatus::from(resp.data.status), + }), + ..item.data }), - ..item.data - }) + PaystackRefundsResponse::PaystackRSyncWebhook(resp) => Ok(Self { + response: Ok(RefundsResponseData { + connector_refund_id: resp.id, + refund_status: enums::RefundStatus::from(resp.status), + }), + ..item.data + }), + PaystackRefundsResponse::PaystackRefundsError(err) => { + let err_msg = get_error_message(err.clone()); + Ok(Self { + response: Err(ErrorResponse { + code: err.code, + message: err_msg.clone(), + reason: Some(err_msg.clone()), + attempt_status: None, + connector_transaction_id: None, + status_code: item.http_code, + }), + ..item.data + }) + } + } } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct PaystackErrorResponse { - pub status_code: u16, - pub code: String, + pub status: bool, pub message: String, - pub reason: Option, + pub data: Option, + pub meta: serde_json::Value, + pub code: String, +} + +pub fn get_error_message(response: PaystackErrorResponse) -> String { + if let Some(serde_json::Value::Object(err_map)) = response.data { + err_map.get("message").map(|msg| msg.clone().to_string()) + } else { + None + } + .unwrap_or(response.message) +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct PaystackPaymentWebhookData { + pub status: PaystackPSyncStatus, + pub reference: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct PaystackRefundWebhookData { + pub status: PaystackRefundStatus, + pub id: String, + pub transaction_reference: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(untagged)] +pub enum PaystackWebhookEventData { + Payment(PaystackPaymentWebhookData), + Refund(PaystackRefundWebhookData), +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct PaystackWebhookData { + pub event: String, + pub data: PaystackWebhookEventData, +} + +impl From for api_models::webhooks::IncomingWebhookEvent { + fn from(item: PaystackWebhookEventData) -> Self { + match item { + PaystackWebhookEventData::Payment(payment_data) => match payment_data.status { + PaystackPSyncStatus::Success => Self::PaymentIntentSuccess, + PaystackPSyncStatus::Failed => Self::PaymentIntentFailure, + PaystackPSyncStatus::Abandoned + | PaystackPSyncStatus::Ongoing + | PaystackPSyncStatus::Pending + | PaystackPSyncStatus::Processing + | PaystackPSyncStatus::Queued => Self::PaymentIntentProcessing, + PaystackPSyncStatus::Reversed => Self::EventNotSupported, + }, + PaystackWebhookEventData::Refund(refund_data) => match refund_data.status { + PaystackRefundStatus::Processed => Self::RefundSuccess, + PaystackRefundStatus::Failed => Self::RefundFailure, + PaystackRefundStatus::Processing | PaystackRefundStatus::Pending => { + Self::EventNotSupported + } + }, + } + } }