Bug: Implement the PaymentAuthorize, PaymentSync, Refund and RefundSync flows for Paystack



Follow the steps outlined in the `add_connector.md` file. 

Implement the PaymentAuthorize, PaymentSync, Refund, RefundSync, and Webhooks flows for Paystack connector.
Connector doc (Paystack): https://paystack.com/docs/payments/payment-channels/

- **Payment Authorize:** 
Authorize call to be made via Electronic Fund Transfer (payment method).
Implement `impl ConnectorIntegration<Authorize, PaymentsAuthorizeData, PaymentsResponseData> for Paystack` to make Payment Authorize call.
- **Payment Sync:**
Implement `impl ConnectorIntegration<PSync, PaymentsSyncData, PaymentsResponseData> for Paystack` in `paystack.rs` to make Payment Sync call.
- **Refund:**
Implement `impl ConnectorIntegration<Execute, RefundsData, RefundsResponseData> for Paystack` in `paystack.rs` to make Refund call.
- **RefundSync:**
Implement `impl ConnectorIntegration<RSync, RefundsData, RefundsResponseData> for Paystack` in `paystack.rs` to make Refund Sync call.
- **Webhooks:**
There are two types of webhooks Payment, and Refund which to be implemented in `impl webhooks::IncomingWebhook for Paystack` in `paystack.rs` to receive the incoming webhooks sent by the connector.

Modify the `get_headers()` and `get_url()` according to the connector documentation.

Implement the Request and Response types and proper status mapping(Payment and Refund) in the `transformers.rs` file.

Webhooks -  Paystack sends a webhook with payment and refund status, we have to catch those and update in the db as well.

Run the following commands before raising the PR -

`cargo clippy`
`cargo +nightly fmt --all`

Ref PR: https://github.com/juspay/hyperswitch/pull/7286
diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml
index b63c4eea2a1..1faac62d6c6 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 a2e01c524e8..5123c66ea7e 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 8084bd00b00..a036e6c4fbd 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 a88b60cc210..6d6ce2ff83a 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<Output = StringMinorUnit> + Sync),
+    amount_converter: &'static (dyn AmountConvertor<Output = MinorUnit> + 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<Authorize, PaymentsAuthorizeData, PaymentsResponseData
     fn get_url(
         &self,
         _req: &PaymentsAuthorizeRouterData,
-        _connectors: &Connectors,
+        connectors: &Connectors,
     ) -> CustomResult<String, errors::ConnectorError> {
-        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<PSync, PaymentsSyncData, PaymentsResponseData> for Pay
 
     fn get_url(
         &self,
-        _req: &PaymentsSyncRouterData,
-        _connectors: &Connectors,
+        req: &PaymentsSyncRouterData,
+        connectors: &Connectors,
     ) -> CustomResult<String, errors::ConnectorError> {
-        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<PSync, PaymentsSyncData, PaymentsResponseData> for Pay
         event_builder: Option<&mut ConnectorEvent>,
         res: Response,
     ) -> CustomResult<PaymentsSyncRouterData, errors::ConnectorError> {
-        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<Execute, RefundsData, RefundsResponseData> for Paystac
     fn get_url(
         &self,
         _req: &RefundsRouterData<Execute>,
-        _connectors: &Connectors,
+        connectors: &Connectors,
     ) -> CustomResult<String, errors::ConnectorError> {
-        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<Execute, RefundsData, RefundsResponseData> for Paystac
         event_builder: Option<&mut ConnectorEvent>,
         res: Response,
     ) -> CustomResult<RefundsRouterData<Execute>, 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<RSync, RefundsData, RefundsResponseData> for Paystack
 
     fn get_url(
         &self,
-        _req: &RefundSyncRouterData,
-        _connectors: &Connectors,
+        req: &RefundSyncRouterData,
+        connectors: &Connectors,
     ) -> CustomResult<String, errors::ConnectorError> {
-        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<RSync, RefundsData, RefundsResponseData> for Paystack
         event_builder: Option<&mut ConnectorEvent>,
         res: Response,
     ) -> CustomResult<RefundSyncRouterData, errors::ConnectorError> {
-        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<RSync, RefundsData, RefundsResponseData> 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<Box<dyn crypto::VerifySignature + Send>, 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<Vec<u8>, 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<Vec<u8>, 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<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
-        Err(report!(errors::ConnectorError::WebhooksNotImplemented))
+        let webhook_body = request
+            .body
+            .parse_struct::<paystack::PaystackWebhookData>("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<api_models::webhooks::IncomingWebhookEvent, errors::ConnectorError> {
-        Err(report!(errors::ConnectorError::WebhooksNotImplemented))
+        let webhook_body = request
+            .body
+            .parse_struct::<paystack::PaystackWebhookData>("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<Box<dyn masking::ErasedMaskSerialize>, errors::ConnectorError> {
-        Err(report!(errors::ConnectorError::WebhooksNotImplemented))
+        let webhook_body = request
+            .body
+            .parse_struct::<paystack::PaystackWebhookData>("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 8a07997fbdc..2ced7d44d54 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<T> {
-    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<T> From<(StringMinorUnit, T)> for PaystackRouterData<T> {
-    fn from((amount, item): (StringMinorUnit, T)) -> Self {
+impl<T> From<(MinorUnit, T)> for PaystackRouterData<T> {
+    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<T> From<(StringMinorUnit, T)> for PaystackRouterData<T> {
     }
 }
 
-//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<String>,
-    expiry_year: Secret<String>,
-    cvc: Secret<String>,
-    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<Self, Self::Error> {
         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<String>,
 }
@@ -90,139 +83,383 @@ impl TryFrom<&ConnectorAuthType> for PaystackAuthType {
         }
     }
 }
-// PaymentsResponse
-//TODO: Append the remaining status flags
-#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
+
+#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
+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<F, T> TryFrom<ResponseRouterData<F, PaystackPaymentsResponse, T, PaymentsResponseData>>
+    for RouterData<F, T, PaymentsResponseData>
+{
+    type Error = error_stack::Report<errors::ConnectorError>;
+    fn try_from(
+        item: ResponseRouterData<F, PaystackPaymentsResponse, T, PaymentsResponseData>,
+    ) -> Result<Self, Self::Error> {
+        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,
+            response,
+            ..item.data
+        })
+    }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
 #[serde(rename_all = "lowercase")]
-pub enum PaystackPaymentStatus {
-    Succeeded,
+pub enum PaystackPSyncStatus {
+    Abandoned,
     Failed,
-    #[default]
+    Ongoing,
+    Pending,
     Processing,
+    Queued,
+    Reversed,
+    Success,
 }
 
-impl From<PaystackPaymentStatus> for common_enums::AttemptStatus {
-    fn from(item: PaystackPaymentStatus) -> Self {
+impl From<PaystackPSyncStatus> for common_enums::AttemptStatus {
+    fn from(item: PaystackPSyncStatus) -> Self {
         match item {
-            PaystackPaymentStatus::Succeeded => Self::Charged,
-            PaystackPaymentStatus::Failed => Self::Failure,
-            PaystackPaymentStatus::Processing => Self::Authorizing,
+            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,
         }
     }
 }
 
-//TODO: Fill the struct with respective fields
-#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
-pub struct PaystackPaymentsResponse {
-    status: PaystackPaymentStatus,
-    id: String,
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+pub struct PaystackPSyncData {
+    status: PaystackPSyncStatus,
+    reference: String,
 }
 
-impl<F, T> TryFrom<ResponseRouterData<F, PaystackPaymentsResponse, T, PaymentsResponseData>>
+#[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<F, T> TryFrom<ResponseRouterData<F, PaystackPSyncResponse, T, PaymentsResponseData>>
     for RouterData<F, T, PaymentsResponseData>
 {
     type Error = error_stack::Report<errors::ConnectorError>;
     fn try_from(
-        item: ResponseRouterData<F, PaystackPaymentsResponse, T, PaymentsResponseData>,
+        item: ResponseRouterData<F, PaystackPSyncResponse, T, PaymentsResponseData>,
     ) -> Result<Self, Self::Error> {
-        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,
+        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
             }),
-            ..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
+                })
+            }
+        }
     }
 }
 
-//TODO: Fill the struct with respective fields
-// REFUND :
-// Type definition for RefundRequest
-#[derive(Default, Debug, Serialize)]
+#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
 pub struct PaystackRefundRequest {
-    pub amount: StringMinorUnit,
+    pub transaction: String,
+    pub amount: MinorUnit,
 }
 
 impl<F> TryFrom<&PaystackRouterData<&RefundsRouterData<F>>> for PaystackRefundRequest {
     type Error = error_stack::Report<errors::ConnectorError>;
     fn try_from(item: &PaystackRouterData<&RefundsRouterData<F>>) -> Result<Self, Self::Error> {
         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<RefundStatus> for enums::RefundStatus {
-    fn from(item: RefundStatus) -> Self {
+impl From<PaystackRefundStatus> 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,
+}
+
+#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
+pub struct PaystackRefundsResponseData {
+    status: bool,
+    message: String,
+    data: PaystackRefundsData,
 }
 
-impl TryFrom<RefundsResponseRouterData<Execute, RefundResponse>> for RefundsRouterData<Execute> {
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+#[serde(untagged)]
+pub enum PaystackRefundsResponse {
+    PaystackRefundsData(PaystackRefundsResponseData),
+    PaystackRSyncWebhook(PaystackRefundWebhookData),
+    PaystackRefundsError(PaystackErrorResponse),
+}
+
+impl TryFrom<RefundsResponseRouterData<Execute, PaystackRefundsResponse>>
+    for RefundsRouterData<Execute>
+{
     type Error = error_stack::Report<errors::ConnectorError>;
     fn try_from(
-        item: RefundsResponseRouterData<Execute, RefundResponse>,
+        item: RefundsResponseRouterData<Execute, PaystackRefundsResponse>,
     ) -> Result<Self, Self::Error> {
-        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<RefundsResponseRouterData<RSync, RefundResponse>> for RefundsRouterData<RSync> {
+impl TryFrom<RefundsResponseRouterData<RSync, PaystackRefundsResponse>>
+    for RefundsRouterData<RSync>
+{
     type Error = error_stack::Report<errors::ConnectorError>;
     fn try_from(
-        item: RefundsResponseRouterData<RSync, RefundResponse>,
+        item: RefundsResponseRouterData<RSync, PaystackRefundsResponse>,
     ) -> Result<Self, Self::Error> {
-        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<String>,
+    pub data: Option<serde_json::Value>,
+    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<PaystackWebhookEventData> 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
+                }
+            },
+        }
+    }
 }

## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [x] New feature
- [ ] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
<!-- Describe your changes in detail -->
Paystack EFT flow (Payments, PSync, Refunds, RSync, Webhooks).


### Additional Changes

- [ ] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless it is an obvious bug or documentation fix
that will have little conversation).
-->


## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->
1. Merchant Account Create:
```
curl --location 'http://localhost:8080/accounts' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: test_admin' \
--data-raw '{
  "merchant_id": "merchant_1739960795",
  "locker_id": "m0010",
  "merchant_name": "NewAge Retailer",
  "merchant_details": {
    "primary_contact_person": "John Test",
    "primary_email": "JohnTest@test.com",
    "primary_phone": "sunt laborum",
    "secondary_contact_person": "John Test2",
    "secondary_email": "JohnTest2@test.com",
    "secondary_phone": "cillum do dolor id",
    "website": "https://www.example.com",
    "about_business": "Online Retail with a wide selection of organic products for North America",
    "address": {
      "line1": "1467",
      "line2": "Harrison Street",
      "line3": "Harrison Street",
      "city": "San Fransico",
      "state": "California",
      "zip": "94122",
      "country": "US"
    }
  },
  "return_url": "https://google.com/success",
  "webhook_details": {
    "webhook_version": "1.0.1",
    "webhook_username": "ekart_retail",
    "webhook_password": "password_ekart@123",
    "payment_created_enabled": true,
    "payment_succeeded_enabled": true,
    "payment_failed_enabled": true
  },
  "sub_merchants_enabled": false,
  "metadata": {
    "city": "NY",
    "unit": "245"
  },
  "primary_business_details": [
    {
      "country": "US",
      "business": "food"
    }
  ]
}'
```

2. API Key Create:
```
curl --location 'http://localhost:8080/api_keys/merchant_1739898797' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: test_admin' \
--data '{
  "name": "API Key 1",
  "description": null,
  "expiration": "2025-09-23T01:02:03.000Z"
}'
```

3. Paystack Connector Create:
```
curl --location 'http://localhost:8080/account/merchant_1739898797/connectors?=' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: test_admin' \
--data '{
    "connector_type": "payment_processor",
    "connector_name": "paystack",
    "connector_account_details": {
        "auth_type": "HeaderKey",
        "api_key": "abc"
    },
    "test_mode": false,
    "disabled": false,
    "payment_methods_enabled": [
        {
                "payment_method": "bank_redirect",
                "payment_method_types": [
                    {
                        "payment_method_type": "eft",
                        "payment_experience": "redirect_to_url",
                        "card_networks": null,
                        "accepted_currencies": null,
                        "accepted_countries": null,
                        "minimum_amount": 0,
                        "maximum_amount": 68607706,
                        "recurring_enabled": true,
                        "installment_payment_enabled": false
                    }
                ]
            }
    ],
    "connector_webhook_details": {
        "merchant_secret": "abc",
        "additional_secret": null
    }
}'
```

4. Payments Create:
Request:
```
curl --location 'http://localhost:8080/payments' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_ykM0z3Y0k65EtZIce9BsbJXPtoq7KzcAWsLZUS3SIT13cdYEOQQ4mMkb4UMB5Oyd' \
--data-raw '{
  "amount": 6540,
  "currency": "ZAR",
  "confirm": true,
  "amount_to_capture": 6540,
  "customer_id": "StripbmnxeCustomer",
  "email": "abc@gmail.com",
  "description": "Its my first payment request",
  "return_url": "https://google.com",
  "payment_method": "bank_redirect",
  "payment_method_type": "eft",
  "payment_method_data": {
    "bank_redirect": {
      "eft": {
        "provider": "ozow"
      }
    }
  }
}'
```
Response:
```
{
    "payment_id": "pay_kQrdGr1rVMmwEKYXorLM",
    "merchant_id": "merchant_1739898797",
    "status": "requires_customer_action",
    "amount": 6540,
    "net_amount": 6540,
    "shipping_cost": null,
    "amount_capturable": 6540,
    "amount_received": null,
    "connector": "paystack",
    "client_secret": "pay_kQrdGr1rVMmwEKYXorLM_secret_U7lStVK9TuKNY4l0mxmc",
    "created": "2025-02-19T10:32:31.775Z",
    "currency": "ZAR",
    "customer_id": "StripbmnxeCustomer",
    "customer": {
        "id": "StripbmnxeCustomer",
        "name": null,
        "email": "abc@gmail.com",
        "phone": null,
        "phone_country_code": null
    },
    "description": "Its my first payment request",
    "refunds": null,
    "disputes": null,
    "mandate_id": null,
    "mandate_data": null,
    "setup_future_usage": null,
    "off_session": null,
    "capture_on": null,
    "capture_method": null,
    "payment_method": "bank_redirect",
    "payment_method_data": {
        "bank_redirect": {
            "type": "BankRedirectResponse",
            "bank_name": null
        },
        "billing": null
    },
    "payment_token": null,
    "shipping": null,
    "billing": null,
    "order_details": null,
    "email": "abc@gmail.com",
    "name": null,
    "phone": null,
    "return_url": "https://google.com/",
    "authentication_type": "no_three_ds",
    "statement_descriptor_name": null,
    "statement_descriptor_suffix": null,
    "next_action": {
        "type": "redirect_to_url",
        "redirect_to_url": "http://localhost:8080/payments/redirect/pay_kQrdGr1rVMmwEKYXorLM/merchant_1739898797/pay_kQrdGr1rVMmwEKYXorLM_1"
    },
    "cancellation_reason": null,
    "error_code": null,
    "error_message": null,
    "unified_code": null,
    "unified_message": null,
    "payment_experience": null,
    "payment_method_type": "eft",
    "connector_label": null,
    "business_country": null,
    "business_label": "default",
    "business_sub_label": null,
    "allowed_payment_method_types": null,
    "ephemeral_key": {
        "customer_id": "StripbmnxeCustomer",
        "created_at": 1739961151,
        "expires": 1739964751,
        "secret": "epk_09592792e8504aa3a943feff38c3e3e2"
    },
    "manual_retry_allowed": null,
    "connector_transaction_id": "sug0jyf5kbbbpe5",
    "frm_message": null,
    "metadata": null,
    "connector_metadata": null,
    "feature_metadata": null,
    "reference_id": null,
    "payment_link": null,
    "profile_id": "pro_z5wMP3CJUUrMDF5CBGl9",
    "surcharge_details": null,
    "attempt_count": 1,
    "merchant_decision": null,
    "merchant_connector_id": "mca_Ux3UO8AL0iD9BlzLSE83",
    "incremental_authorization_allowed": null,
    "authorization_count": null,
    "incremental_authorizations": null,
    "external_authentication_details": null,
    "external_3ds_authentication_attempted": false,
    "expires_on": "2025-02-19T10:47:31.774Z",
    "fingerprint": null,
    "browser_info": null,
    "payment_method_id": null,
    "payment_method_status": null,
    "updated": "2025-02-19T10:32:32.739Z",
    "split_payments": null,
    "frm_metadata": null,
    "merchant_order_reference_id": null,
    "order_tax_amount": null,
    "connector_mandate_id": null
}
```

5. Payments Retrieve
Request:
```
curl --location 'http://localhost:8080/payments/pay_kWVz157PfvGL9GyAiTnI?force_sync=true' \
--header 'Accept: application/json' \
--header 'api-key: dev_ykM0z3Y0k65EtZIce9BsbJXPtoq7KzcAWsLZUS3SIT13cdYEOQQ4mMkb4UMB5Oyd' \
--data ''
```
Response: Same as payments response>

6. Refunds Create
Request:
```
curl --location 'http://localhost:8080/refunds' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_ykM0z3Y0k65EtZIce9BsbJXPtoq7KzcAWsLZUS3SIT13cdYEOQQ4mMkb4UMB5Oyd' \
--data '{
  "payment_id": "pay_6Ww3WmcWCT8ni7O5K5gV",
  "amount": 1000,
  "reason": "Customer returned product",
  "refund_type": "instant",
  "metadata": {
    "udf1": "value1",
    "new_customer": "true",
    "login_date": "2019-09-10T10:11:12Z"
  }
}'
```
Response:
```
{
    "refund_id": "ref_XSsCj6O6bujAZypJAFJk",
    "payment_id": "pay_6Ww3WmcWCT8ni7O5K5gV",
    "amount": 1000,
    "currency": "ZAR",
    "status": "succeeded",
    "reason": "Customer returned product",
    "metadata": {
        "udf1": "value1",
        "new_customer": "true",
        "login_date": "2019-09-10T10:11:12Z"
    },
    "error_message": null,
    "error_code": null,
    "unified_code": null,
    "unified_message": null,
    "created_at": "2025-02-19T10:31:32.658Z",
    "updated_at": "2025-02-19T10:31:34.094Z",
    "connector": "paystack",
    "profile_id": "pro_z5wMP3CJUUrMDF5CBGl9",
    "merchant_connector_id": "mca_Ux3UO8AL0iD9BlzLSE83",
    "split_refunds": null
}
```

7. Refunds Retrieve
Request:
```
curl --location 'http://localhost:8080/refunds/ref_XSsCj6O6bujAZypJAFJk' \
--header 'Accept: application/json' \
--header 'api-key: dev_ZgTXlFsnanc0o4AW2Ag32dJQsA1rAvU1prQVsIE0JbUjPv7g65ZU5pO7MyiPLL5J'
```
Response: Same as Refunds response.

## Checklist
<!-- Put an `x` in the boxes that apply -->

- [x] I formatted the code `cargo +nightly fmt --all`
- [x] I addressed lints thrown by `cargo clippy`
- [x] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
