mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 09:07:09 +08:00
feat(Connector): [VOLT] Add support for Payments Webhooks (#3155)
This commit is contained in:
@ -2,11 +2,13 @@ pub mod transformers;
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use common_utils::request::RequestContent;
|
||||
use common_utils::{crypto, ext_traits::ByteSliceExt, request::RequestContent};
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use masking::{ExposeInterface, PeekInterface};
|
||||
use transformers as volt;
|
||||
|
||||
use self::transformers::webhook_headers;
|
||||
use super::utils;
|
||||
use crate::{
|
||||
configs::settings,
|
||||
core::errors::{self, CustomResult},
|
||||
@ -398,7 +400,7 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
|
||||
data: &types::PaymentsSyncRouterData,
|
||||
res: Response,
|
||||
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
|
||||
let response: volt::VoltPsyncResponse = res
|
||||
let response: volt::VoltPaymentsResponseData = res
|
||||
.response
|
||||
.parse_struct("volt PaymentsSyncResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
@ -586,24 +588,93 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl api::IncomingWebhook for Volt {
|
||||
fn get_webhook_object_reference_id(
|
||||
fn get_webhook_source_verification_algorithm(
|
||||
&self,
|
||||
_request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<Box<dyn crypto::VerifySignature + Send>, errors::ConnectorError> {
|
||||
Ok(Box::new(crypto::HmacSha256))
|
||||
}
|
||||
|
||||
fn get_webhook_source_verification_signature(
|
||||
&self,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
|
||||
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
||||
let signature =
|
||||
utils::get_header_key_value(webhook_headers::X_VOLT_SIGNED, request.headers)
|
||||
.change_context(errors::ConnectorError::WebhookSignatureNotFound)?;
|
||||
|
||||
hex::decode(signature)
|
||||
.into_report()
|
||||
.change_context(errors::ConnectorError::WebhookVerificationSecretInvalid)
|
||||
}
|
||||
|
||||
fn get_webhook_source_verification_message(
|
||||
&self,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
_merchant_id: &str,
|
||||
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
|
||||
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
||||
let x_volt_timed =
|
||||
utils::get_header_key_value(webhook_headers::X_VOLT_TIMED, request.headers)?;
|
||||
let user_agent = utils::get_header_key_value(webhook_headers::USER_AGENT, request.headers)?;
|
||||
let version = user_agent
|
||||
.split('/')
|
||||
.last()
|
||||
.ok_or(errors::ConnectorError::WebhookSourceVerificationFailed)?;
|
||||
Ok(format!(
|
||||
"{}|{}|{}",
|
||||
String::from_utf8_lossy(request.body),
|
||||
x_volt_timed,
|
||||
version
|
||||
)
|
||||
.into_bytes())
|
||||
}
|
||||
|
||||
fn get_webhook_object_reference_id(
|
||||
&self,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<api::webhooks::ObjectReferenceId, errors::ConnectorError> {
|
||||
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
|
||||
let webhook_body: volt::VoltWebhookBodyReference = request
|
||||
.body
|
||||
.parse_struct("VoltWebhookBodyReference")
|
||||
.change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
|
||||
let reference = match webhook_body.merchant_internal_reference {
|
||||
Some(merchant_internal_reference) => {
|
||||
api_models::payments::PaymentIdType::PaymentAttemptId(merchant_internal_reference)
|
||||
}
|
||||
None => {
|
||||
api_models::payments::PaymentIdType::ConnectorTransactionId(webhook_body.payment)
|
||||
}
|
||||
};
|
||||
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
|
||||
reference,
|
||||
))
|
||||
}
|
||||
|
||||
fn get_webhook_event_type(
|
||||
&self,
|
||||
_request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
|
||||
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
|
||||
if request.body.is_empty() {
|
||||
Ok(api::IncomingWebhookEvent::EndpointVerification)
|
||||
} else {
|
||||
let payload: volt::VoltWebhookBodyEventType = request
|
||||
.body
|
||||
.parse_struct("VoltWebhookBodyEventType")
|
||||
.change_context(errors::ConnectorError::WebhookEventTypeNotFound)?;
|
||||
Ok(api::IncomingWebhookEvent::from(payload.status))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_webhook_resource_object(
|
||||
&self,
|
||||
_request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<Box<dyn masking::ErasedMaskSerialize>, errors::ConnectorError> {
|
||||
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
|
||||
let details: volt::VoltWebhookObjectResource = request
|
||||
.body
|
||||
.parse_struct("VoltWebhookObjectResource")
|
||||
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?;
|
||||
Ok(Box::new(details))
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
connector::utils::{self, AddressDetailsData, RouterData},
|
||||
consts,
|
||||
core::errors,
|
||||
services,
|
||||
types::{self, api, storage::enums as storage_enums},
|
||||
@ -41,6 +42,12 @@ impl<T>
|
||||
}
|
||||
}
|
||||
|
||||
pub mod webhook_headers {
|
||||
pub const X_VOLT_SIGNED: &str = "X-Volt-Signed";
|
||||
pub const X_VOLT_TIMED: &str = "X-Volt-Timed";
|
||||
pub const USER_AGENT: &str = "User-Agent";
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VoltPaymentsRequest {
|
||||
@ -50,7 +57,6 @@ pub struct VoltPaymentsRequest {
|
||||
transaction_type: TransactionType,
|
||||
merchant_internal_reference: String,
|
||||
shopper: ShopperDetails,
|
||||
notification_url: Option<String>,
|
||||
payment_success_url: Option<String>,
|
||||
payment_failure_url: Option<String>,
|
||||
payment_pending_url: Option<String>,
|
||||
@ -91,7 +97,6 @@ impl TryFrom<&VoltRouterData<&types::PaymentsAuthorizeRouterData>> for VoltPayme
|
||||
let payment_failure_url = item.router_data.request.router_return_url.clone();
|
||||
let payment_pending_url = item.router_data.request.router_return_url.clone();
|
||||
let payment_cancel_url = item.router_data.request.router_return_url.clone();
|
||||
let notification_url = item.router_data.request.webhook_url.clone();
|
||||
let address = item.router_data.get_billing_address()?;
|
||||
let shopper = ShopperDetails {
|
||||
email: item.router_data.request.email.clone(),
|
||||
@ -109,7 +114,6 @@ impl TryFrom<&VoltRouterData<&types::PaymentsAuthorizeRouterData>> for VoltPayme
|
||||
payment_failure_url,
|
||||
payment_pending_url,
|
||||
payment_cancel_url,
|
||||
notification_url,
|
||||
shopper,
|
||||
transaction_type,
|
||||
})
|
||||
@ -291,8 +295,9 @@ impl<F, T>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Serialize, Clone, Deserialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
#[derive(strum::Display)]
|
||||
pub enum VoltPaymentStatus {
|
||||
NewPayment,
|
||||
Completed,
|
||||
@ -309,7 +314,15 @@ pub enum VoltPaymentStatus {
|
||||
Failed,
|
||||
Settled,
|
||||
}
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum VoltPaymentsResponseData {
|
||||
WebhookResponse(VoltWebhookObjectResource),
|
||||
PsyncResponse(VoltPsyncResponse),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VoltPsyncResponse {
|
||||
status: VoltPaymentStatus,
|
||||
@ -317,29 +330,102 @@ pub struct VoltPsyncResponse {
|
||||
merchant_internal_reference: Option<String>,
|
||||
}
|
||||
|
||||
impl<F, T> TryFrom<types::ResponseRouterData<F, VoltPsyncResponse, T, types::PaymentsResponseData>>
|
||||
impl<F, T>
|
||||
TryFrom<types::ResponseRouterData<F, VoltPaymentsResponseData, T, types::PaymentsResponseData>>
|
||||
for types::RouterData<F, T, types::PaymentsResponseData>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::ResponseRouterData<F, VoltPsyncResponse, T, types::PaymentsResponseData>,
|
||||
item: types::ResponseRouterData<
|
||||
F,
|
||||
VoltPaymentsResponseData,
|
||||
T,
|
||||
types::PaymentsResponseData,
|
||||
>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
status: enums::AttemptStatus::from(item.response.status),
|
||||
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()),
|
||||
redirection_data: None,
|
||||
mandate_reference: None,
|
||||
connector_metadata: None,
|
||||
network_txn_id: None,
|
||||
connector_response_reference_id: item
|
||||
.response
|
||||
.merchant_internal_reference
|
||||
.or(Some(item.response.id)),
|
||||
incremental_authorization_allowed: None,
|
||||
}),
|
||||
..item.data
|
||||
})
|
||||
match item.response {
|
||||
VoltPaymentsResponseData::PsyncResponse(payment_response) => {
|
||||
let status = enums::AttemptStatus::from(payment_response.status.clone());
|
||||
Ok(Self {
|
||||
status,
|
||||
response: if is_payment_failure(status) {
|
||||
Err(types::ErrorResponse {
|
||||
code: payment_response.status.clone().to_string(),
|
||||
message: payment_response.status.clone().to_string(),
|
||||
reason: Some(payment_response.status.to_string()),
|
||||
status_code: item.http_code,
|
||||
attempt_status: None,
|
||||
connector_transaction_id: Some(payment_response.id),
|
||||
})
|
||||
} else {
|
||||
Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: types::ResponseId::ConnectorTransactionId(
|
||||
payment_response.id.clone(),
|
||||
),
|
||||
redirection_data: None,
|
||||
mandate_reference: None,
|
||||
connector_metadata: None,
|
||||
network_txn_id: None,
|
||||
connector_response_reference_id: payment_response
|
||||
.merchant_internal_reference
|
||||
.or(Some(payment_response.id)),
|
||||
incremental_authorization_allowed: None,
|
||||
})
|
||||
},
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
VoltPaymentsResponseData::WebhookResponse(webhook_response) => {
|
||||
let detailed_status = webhook_response.detailed_status.clone();
|
||||
let status = enums::AttemptStatus::from(webhook_response.status);
|
||||
Ok(Self {
|
||||
status,
|
||||
response: if is_payment_failure(status) {
|
||||
Err(types::ErrorResponse {
|
||||
code: detailed_status
|
||||
.clone()
|
||||
.map(|volt_status| volt_status.to_string())
|
||||
.unwrap_or_else(|| consts::NO_ERROR_CODE.to_owned()),
|
||||
message: detailed_status
|
||||
.clone()
|
||||
.map(|volt_status| volt_status.to_string())
|
||||
.unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_owned()),
|
||||
reason: detailed_status
|
||||
.clone()
|
||||
.map(|volt_status| volt_status.to_string()),
|
||||
status_code: item.http_code,
|
||||
attempt_status: None,
|
||||
connector_transaction_id: Some(webhook_response.payment.clone()),
|
||||
})
|
||||
} else {
|
||||
Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: types::ResponseId::ConnectorTransactionId(
|
||||
webhook_response.payment.clone(),
|
||||
),
|
||||
redirection_data: None,
|
||||
mandate_reference: None,
|
||||
connector_metadata: None,
|
||||
network_txn_id: None,
|
||||
connector_response_reference_id: webhook_response
|
||||
.merchant_internal_reference
|
||||
.or(Some(webhook_response.payment)),
|
||||
incremental_authorization_allowed: None,
|
||||
})
|
||||
},
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VoltWebhookStatus> for enums::AttemptStatus {
|
||||
fn from(status: VoltWebhookStatus) -> Self {
|
||||
match status {
|
||||
VoltWebhookStatus::Completed | VoltWebhookStatus::Received => Self::Charged,
|
||||
VoltWebhookStatus::Failed | VoltWebhookStatus::NotReceived => Self::Failure,
|
||||
VoltWebhookStatus::Pending => Self::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -405,6 +491,68 @@ impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
pub struct VoltWebhookBodyReference {
|
||||
pub payment: String,
|
||||
pub merchant_internal_reference: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VoltWebhookBodyEventType {
|
||||
pub status: VoltWebhookStatus,
|
||||
pub detailed_status: Option<VoltDetailedStatus>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VoltWebhookObjectResource {
|
||||
pub payment: String,
|
||||
pub merchant_internal_reference: Option<String>,
|
||||
pub status: VoltWebhookStatus,
|
||||
pub detailed_status: Option<VoltDetailedStatus>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum VoltWebhookStatus {
|
||||
Completed,
|
||||
Failed,
|
||||
Pending,
|
||||
Received,
|
||||
NotReceived,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
#[derive(strum::Display)]
|
||||
pub enum VoltDetailedStatus {
|
||||
RefusedByRisk,
|
||||
RefusedByBank,
|
||||
ErrorAtBank,
|
||||
CancelledByUser,
|
||||
AbandonedByUser,
|
||||
Failed,
|
||||
Completed,
|
||||
BankRedirect,
|
||||
DelayedAtBank,
|
||||
AwaitingCheckoutAuthorisation,
|
||||
}
|
||||
|
||||
impl From<VoltWebhookStatus> for api::IncomingWebhookEvent {
|
||||
fn from(status: VoltWebhookStatus) -> Self {
|
||||
match status {
|
||||
VoltWebhookStatus::Completed | VoltWebhookStatus::Received => {
|
||||
Self::PaymentIntentSuccess
|
||||
}
|
||||
VoltWebhookStatus::Failed | VoltWebhookStatus::NotReceived => {
|
||||
Self::PaymentIntentFailure
|
||||
}
|
||||
VoltWebhookStatus::Pending => Self::PaymentIntentProcessing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct VoltErrorResponse {
|
||||
pub exception: VoltErrorException,
|
||||
@ -429,3 +577,32 @@ pub struct VoltErrorList {
|
||||
pub property: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
fn is_payment_failure(status: enums::AttemptStatus) -> bool {
|
||||
match status {
|
||||
common_enums::AttemptStatus::AuthenticationFailed
|
||||
| common_enums::AttemptStatus::AuthorizationFailed
|
||||
| common_enums::AttemptStatus::CaptureFailed
|
||||
| common_enums::AttemptStatus::VoidFailed
|
||||
| common_enums::AttemptStatus::Failure => true,
|
||||
common_enums::AttemptStatus::Started
|
||||
| common_enums::AttemptStatus::RouterDeclined
|
||||
| common_enums::AttemptStatus::AuthenticationPending
|
||||
| common_enums::AttemptStatus::AuthenticationSuccessful
|
||||
| common_enums::AttemptStatus::Authorized
|
||||
| common_enums::AttemptStatus::Charged
|
||||
| common_enums::AttemptStatus::Authorizing
|
||||
| common_enums::AttemptStatus::CodInitiated
|
||||
| common_enums::AttemptStatus::Voided
|
||||
| common_enums::AttemptStatus::VoidInitiated
|
||||
| common_enums::AttemptStatus::CaptureInitiated
|
||||
| common_enums::AttemptStatus::AutoRefunded
|
||||
| common_enums::AttemptStatus::PartialCharged
|
||||
| common_enums::AttemptStatus::PartialChargedAndChargeable
|
||||
| common_enums::AttemptStatus::Unresolved
|
||||
| common_enums::AttemptStatus::Pending
|
||||
| common_enums::AttemptStatus::PaymentMethodAwaited
|
||||
| common_enums::AttemptStatus::ConfirmationAwaited
|
||||
| common_enums::AttemptStatus::DeviceDataCollectionPending => false,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user