Skip to content

Commit

Permalink
feat(router): add payment incoming webhooks support for v2 (#6551)
Browse files Browse the repository at this point in the history
Co-authored-by: Narayan Bhat <[email protected]>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
Co-authored-by: hrithikesh026 <[email protected]>
  • Loading branch information
4 people authored and Sayak Bhattacharya committed Nov 26, 2024
1 parent dc7cb1c commit 2f65453
Show file tree
Hide file tree
Showing 25 changed files with 1,271 additions and 9 deletions.
99 changes: 97 additions & 2 deletions crates/api_models/src/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3691,6 +3691,7 @@ pub struct PaymentMethodDataResponseWithBilling {
}

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, ToSchema)]
#[cfg(feature = "v1")]
pub enum PaymentIdType {
/// The identifier for payment intent
PaymentIntentId(id_type::PaymentId),
Expand All @@ -3702,6 +3703,20 @@ pub enum PaymentIdType {
PreprocessingId(String),
}

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, ToSchema)]
#[cfg(feature = "v2")]
pub enum PaymentIdType {
/// The identifier for payment intent
PaymentIntentId(id_type::GlobalPaymentId),
/// The identifier for connector transaction
ConnectorTransactionId(String),
/// The identifier for payment attempt
PaymentAttemptId(String),
/// The identifier for preprocessing step
PreprocessingId(String),
}

#[cfg(feature = "v1")]
impl fmt::Display for PaymentIdType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Expand All @@ -3726,6 +3741,7 @@ impl fmt::Display for PaymentIdType {
}
}

#[cfg(feature = "v1")]
impl Default for PaymentIdType {
fn default() -> Self {
Self::PaymentIntentId(Default::default())
Expand Down Expand Up @@ -4566,7 +4582,7 @@ pub struct PaymentsRetrieveRequest {

/// Error details for the payment
#[cfg(feature = "v2")]
#[derive(Debug, serde::Serialize, ToSchema)]
#[derive(Debug, serde::Serialize, Clone, ToSchema)]
pub struct ErrorDetails {
/// The error code
pub code: String,
Expand Down Expand Up @@ -4651,7 +4667,7 @@ pub struct PaymentsConfirmIntentResponse {
// TODO: have a separate response for detailed, summarized
/// Response for Payment Intent Confirm
#[cfg(feature = "v2")]
#[derive(Debug, serde::Serialize, ToSchema)]
#[derive(Debug, serde::Serialize, Clone, ToSchema)]
pub struct PaymentsRetrieveResponse {
/// Unique identifier for the payment. This ensures idempotency for multiple payments
/// that have been done by a single merchant.
Expand Down Expand Up @@ -6307,6 +6323,85 @@ pub struct FrmMessage {
pub frm_error: Option<String>,
}

#[cfg(feature = "v2")]
mod payment_id_type {
use std::{borrow::Cow, fmt};

use serde::{
de::{self, Visitor},
Deserializer,
};

use super::PaymentIdType;

struct PaymentIdVisitor;
struct OptionalPaymentIdVisitor;

impl<'de> Visitor<'de> for PaymentIdVisitor {
type Value = PaymentIdType;

fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("payment id")
}

fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
common_utils::id_type::GlobalPaymentId::try_from(Cow::Owned(value.to_string()))
.map_err(de::Error::custom)
.map(PaymentIdType::PaymentIntentId)
}
}

impl<'de> Visitor<'de> for OptionalPaymentIdVisitor {
type Value = Option<PaymentIdType>;

fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("payment id")
}

fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(PaymentIdVisitor).map(Some)
}

fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(None)
}

fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(None)
}
}

#[allow(dead_code)]
pub(crate) fn deserialize<'a, D>(deserializer: D) -> Result<PaymentIdType, D::Error>
where
D: Deserializer<'a>,
{
deserializer.deserialize_any(PaymentIdVisitor)
}

pub(crate) fn deserialize_option<'a, D>(
deserializer: D,
) -> Result<Option<PaymentIdType>, D::Error>
where
D: Deserializer<'a>,
{
deserializer.deserialize_option(OptionalPaymentIdVisitor)
}
}

#[cfg(feature = "v1")]
mod payment_id_type {
use std::{borrow::Cow, fmt};

Expand Down
51 changes: 51 additions & 0 deletions crates/api_models/src/webhooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,25 +76,45 @@ pub enum WebhookFlow {
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
/// This enum tells about the affect a webhook had on an object
pub enum WebhookResponseTracker {
#[cfg(feature = "v1")]
Payment {
payment_id: common_utils::id_type::PaymentId,
status: common_enums::IntentStatus,
},
#[cfg(feature = "v2")]
Payment {
payment_id: common_utils::id_type::GlobalPaymentId,
status: common_enums::IntentStatus,
},
#[cfg(feature = "payouts")]
Payout {
payout_id: String,
status: common_enums::PayoutStatus,
},
#[cfg(feature = "v1")]
Refund {
payment_id: common_utils::id_type::PaymentId,
refund_id: String,
status: common_enums::RefundStatus,
},
#[cfg(feature = "v2")]
Refund {
payment_id: common_utils::id_type::GlobalPaymentId,
refund_id: String,
status: common_enums::RefundStatus,
},
#[cfg(feature = "v1")]
Dispute {
dispute_id: String,
payment_id: common_utils::id_type::PaymentId,
status: common_enums::DisputeStatus,
},
#[cfg(feature = "v2")]
Dispute {
dispute_id: String,
payment_id: common_utils::id_type::GlobalPaymentId,
status: common_enums::DisputeStatus,
},
Mandate {
mandate_id: String,
status: common_enums::MandateStatus,
Expand All @@ -103,6 +123,7 @@ pub enum WebhookResponseTracker {
}

impl WebhookResponseTracker {
#[cfg(feature = "v1")]
pub fn get_payment_id(&self) -> Option<common_utils::id_type::PaymentId> {
match self {
Self::Payment { payment_id, .. }
Expand All @@ -113,6 +134,18 @@ impl WebhookResponseTracker {
Self::Payout { .. } => None,
}
}

#[cfg(feature = "v2")]
pub fn get_payment_id(&self) -> Option<common_utils::id_type::GlobalPaymentId> {
match self {
Self::Payment { payment_id, .. }
| Self::Refund { payment_id, .. }
| Self::Dispute { payment_id, .. } => Some(payment_id.to_owned()),
Self::NoEffect | Self::Mandate { .. } => None,
#[cfg(feature = "payouts")]
Self::Payout { .. } => None,
}
}
}

impl From<IncomingWebhookEvent> for WebhookFlow {
Expand Down Expand Up @@ -227,6 +260,7 @@ pub struct OutgoingWebhook {

#[derive(Debug, Clone, Serialize, ToSchema)]
#[serde(tag = "type", content = "object", rename_all = "snake_case")]
#[cfg(feature = "v1")]
pub enum OutgoingWebhookContent {
#[schema(value_type = PaymentsResponse, title = "PaymentsResponse")]
PaymentDetails(Box<payments::PaymentsResponse>),
Expand All @@ -241,6 +275,23 @@ pub enum OutgoingWebhookContent {
PayoutDetails(Box<payouts::PayoutCreateResponse>),
}

#[derive(Debug, Clone, Serialize, ToSchema)]
#[serde(tag = "type", content = "object", rename_all = "snake_case")]
#[cfg(feature = "v2")]
pub enum OutgoingWebhookContent {
#[schema(value_type = PaymentsResponse, title = "PaymentsResponse")]
PaymentDetails(Box<payments::PaymentsRetrieveResponse>),
#[schema(value_type = RefundResponse, title = "RefundResponse")]
RefundDetails(Box<refunds::RefundResponse>),
#[schema(value_type = DisputeResponse, title = "DisputeResponse")]
DisputeDetails(Box<disputes::DisputeResponse>),
#[schema(value_type = MandateResponse, title = "MandateResponse")]
MandateDetails(Box<mandates::MandateResponse>),
#[cfg(feature = "payouts")]
#[schema(value_type = PayoutCreateResponse, title = "PayoutCreateResponse")]
PayoutDetails(Box<payouts::PayoutCreateResponse>),
}

#[derive(Debug, Clone, Serialize)]
pub struct ConnectorWebhookSecrets {
pub secret: Vec<u8>,
Expand Down
6 changes: 6 additions & 0 deletions crates/common_utils/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,16 @@ pub enum ApiEventsType {
PaymentMethodList {
payment_id: Option<String>,
},
#[cfg(feature = "v1")]
Webhooks {
connector: String,
payment_id: Option<id_type::PaymentId>,
},
#[cfg(feature = "v2")]
Webhooks {
connector: id_type::MerchantConnectorAccountId,
payment_id: Option<id_type::GlobalPaymentId>,
},
Routing,
ResourceListAPI,
#[cfg(feature = "v1")]
Expand Down
13 changes: 13 additions & 0 deletions crates/common_utils/src/id_type/global_id/payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,16 @@ impl GlobalAttemptId {
self.0.get_string_repr()
}
}

impl TryFrom<std::borrow::Cow<'static, str>> for GlobalAttemptId {
type Error = error_stack::Report<errors::ValidationError>;
fn try_from(value: std::borrow::Cow<'static, str>) -> Result<Self, Self::Error> {
use error_stack::ResultExt;
let global_attempt_id = super::GlobalId::from_string(value).change_context(
errors::ValidationError::IncorrectValueProvided {
field_name: "payment_id",
},
)?;
Ok(Self(global_attempt_id))
}
}
2 changes: 1 addition & 1 deletion crates/diesel_models/src/payment_attempt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,7 @@ pub struct PaymentAttemptUpdateInternal {
pub browser_info: Option<serde_json::Value>,
// payment_token: Option<String>,
pub error_code: Option<String>,
// connector_metadata: Option<serde_json::Value>,
pub connector_metadata: Option<pii::SecretSerdeValue>,
// payment_method_data: Option<serde_json::Value>,
// payment_experience: Option<storage_enums::PaymentExperience>,
// preprocessing_step_id: Option<String>,
Expand Down
27 changes: 27 additions & 0 deletions crates/diesel_models/src/query/payment_attempt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,33 @@ impl PaymentAttempt {
.await
}

#[cfg(feature = "v2")]
pub async fn find_by_profile_id_connector_transaction_id(
conn: &PgPooledConn,
profile_id: &common_utils::id_type::ProfileId,
connector_txn_id: &str,
) -> StorageResult<Self> {
let (txn_id, txn_data) = common_utils::types::ConnectorTransactionId::form_id_and_data(
connector_txn_id.to_string(),
);
let connector_transaction_id = txn_id
.get_txn_id(txn_data.as_ref())
.change_context(DatabaseError::Others)
.attach_printable_lazy(|| {
format!(
"Failed to retrieve txn_id for ({:?}, {:?})",
txn_id, txn_data
)
})?;
generics::generic_find_one::<<Self as HasTable>::Table, _, _>(
conn,
dsl::profile_id
.eq(profile_id.to_owned())
.and(dsl::connector_payment_id.eq(connector_transaction_id.to_owned())),
)
.await
}

#[cfg(feature = "v1")]
pub async fn find_by_merchant_id_attempt_id(
conn: &PgPooledConn,
Expand Down
10 changes: 10 additions & 0 deletions crates/hyperswitch_domain_models/src/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -574,3 +574,13 @@ where
/// This will depend on the payment status and the force sync flag in the request
pub should_sync_with_connector: bool,
}

#[cfg(feature = "v2")]
impl<F> PaymentStatusData<F>
where
F: Clone,
{
pub fn get_payment_id(&self) -> &id_type::GlobalPaymentId {
&self.payment_intent.id
}
}
Loading

0 comments on commit 2f65453

Please sign in to comment.