feat(Connector): [VOLT] Add support for Payments Webhooks (#3155)

This commit is contained in:
Swangi Kumari
2024-01-09 19:54:54 +05:30
committed by GitHub
parent 8a354f4229
commit eba789640b
2 changed files with 279 additions and 31 deletions

View File

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

View File

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