feat(connector): [PAYSTACK] Electronic Fund Transfer(EFT) Payment Flows (#7440)

Co-authored-by: Anurag Singh <anurag.singh.001@Anurag-Singh-WPMHJ5619X.local>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Sagnik Mitra
2025-03-17 12:17:47 +05:30
committed by GitHub
parent 2db2738cf8
commit c39ecda79a
5 changed files with 482 additions and 137 deletions

View File

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

View File

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

View File

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

View File

@ -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)
}
})
}
}

View File

@ -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,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<PaystackPaymentStatus> 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<F, T> TryFrom<ResponseRouterData<F, PaystackPaymentsResponse, T, PaymentsResponseData>>
@ -125,104 +112,354 @@ impl<F, T> TryFrom<ResponseRouterData<F, PaystackPaymentsResponse, T, PaymentsRe
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: 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<PaystackPSyncStatus> 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<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, PaystackPSyncResponse, T, PaymentsResponseData>,
) -> Result<Self, Self::Error> {
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<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,
}
impl TryFrom<RefundsResponseRouterData<Execute, RefundResponse>> for RefundsRouterData<Execute> {
#[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<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
}
},
}
}
}