mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 12:15:40 +08:00
feat(compatibility): add webhook support for stripe compatibility (#710)
This commit is contained in:
@ -62,3 +62,6 @@ pub enum OutgoingWebhookContent {
|
||||
PaymentDetails(payments::PaymentsResponse),
|
||||
RefundDetails(refunds::RefundResponse),
|
||||
}
|
||||
|
||||
pub trait OutgoingWebhookType: Serialize + From<OutgoingWebhook> + Sync + Send {}
|
||||
impl OutgoingWebhookType for OutgoingWebhook {}
|
||||
|
||||
@ -3,6 +3,7 @@ pub mod customers;
|
||||
pub mod payment_intents;
|
||||
pub mod refunds;
|
||||
pub mod setup_intents;
|
||||
pub mod webhooks;
|
||||
use actix_web::{web, Scope};
|
||||
pub mod errors;
|
||||
|
||||
@ -18,6 +19,7 @@ impl StripeApis {
|
||||
.service(app::SetupIntents::server(state.clone()))
|
||||
.service(app::PaymentIntents::server(state.clone()))
|
||||
.service(app::Refunds::server(state.clone()))
|
||||
.service(app::Customers::server(state))
|
||||
.service(app::Customers::server(state.clone()))
|
||||
.service(app::Webhooks::server(state))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use actix_web::{web, Scope};
|
||||
|
||||
use super::{customers::*, payment_intents::*, refunds::*, setup_intents::*};
|
||||
use crate::routes;
|
||||
use super::{customers::*, payment_intents::*, refunds::*, setup_intents::*, webhooks::*};
|
||||
use crate::routes::{self, webhooks};
|
||||
|
||||
pub struct PaymentIntents;
|
||||
|
||||
@ -55,3 +55,21 @@ impl Customers {
|
||||
.service(list_customer_payment_method_api)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Webhooks;
|
||||
|
||||
impl Webhooks {
|
||||
pub fn server(config: routes::AppState) -> Scope {
|
||||
web::scope("/webhooks")
|
||||
.app_data(web::Data::new(config))
|
||||
.service(
|
||||
web::resource("/{merchant_id}/{connector}")
|
||||
.route(
|
||||
web::post().to(webhooks::receive_incoming_webhook::<StripeOutgoingWebhook>),
|
||||
)
|
||||
.route(
|
||||
web::get().to(webhooks::receive_incoming_webhook::<StripeOutgoingWebhook>),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ use std::{convert::From, default::Default};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{core::errors, types::api::refunds};
|
||||
use crate::types::api::refunds;
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct StripeCreateRefundRequest {
|
||||
@ -68,10 +68,9 @@ impl From<refunds::RefundStatus> for StripeRefundStatus {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<refunds::RefundResponse> for StripeCreateRefundResponse {
|
||||
type Error = error_stack::Report<errors::ApiErrorResponse>;
|
||||
fn try_from(res: refunds::RefundResponse) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
impl From<refunds::RefundResponse> for StripeCreateRefundResponse {
|
||||
fn from(res: refunds::RefundResponse) -> Self {
|
||||
Self {
|
||||
id: res.refund_id,
|
||||
amount: res.amount,
|
||||
currency: res.currency.to_ascii_lowercase(),
|
||||
@ -79,6 +78,6 @@ impl TryFrom<refunds::RefundResponse> for StripeCreateRefundResponse {
|
||||
status: res.status.into(),
|
||||
created: res.created_at.map(|t| t.assume_utc().unix_timestamp()),
|
||||
metadata: res.metadata.unwrap_or_else(|| serde_json::json!({})),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
54
crates/router/src/compatibility/stripe/webhooks.rs
Normal file
54
crates/router/src/compatibility/stripe/webhooks.rs
Normal file
@ -0,0 +1,54 @@
|
||||
use api_models::webhooks::{self as api};
|
||||
use serde::Serialize;
|
||||
|
||||
use super::{
|
||||
payment_intents::types::StripePaymentIntentResponse, refunds::types::StripeCreateRefundResponse,
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct StripeOutgoingWebhook {
|
||||
id: Option<String>,
|
||||
#[serde(rename = "type")]
|
||||
stype: &'static str,
|
||||
data: StripeWebhookObject,
|
||||
}
|
||||
|
||||
impl api::OutgoingWebhookType for StripeOutgoingWebhook {}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(tag = "type", content = "object", rename_all = "snake_case")]
|
||||
pub enum StripeWebhookObject {
|
||||
PaymentIntent(StripePaymentIntentResponse),
|
||||
Refund(StripeCreateRefundResponse),
|
||||
}
|
||||
|
||||
impl From<api::OutgoingWebhook> for StripeOutgoingWebhook {
|
||||
fn from(value: api::OutgoingWebhook) -> Self {
|
||||
let data: StripeWebhookObject = value.content.into();
|
||||
Self {
|
||||
id: data.get_id(),
|
||||
stype: "webhook_endpoint",
|
||||
data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::OutgoingWebhookContent> for StripeWebhookObject {
|
||||
fn from(value: api::OutgoingWebhookContent) -> Self {
|
||||
match value {
|
||||
api::OutgoingWebhookContent::PaymentDetails(payment) => {
|
||||
Self::PaymentIntent(payment.into())
|
||||
}
|
||||
api::OutgoingWebhookContent::RefundDetails(refund) => Self::Refund(refund.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StripeWebhookObject {
|
||||
fn get_id(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::PaymentIntent(p) => p.id.to_owned(),
|
||||
Self::Refund(r) => Some(r.id.to_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -26,7 +26,7 @@ use crate::{
|
||||
const OUTGOING_WEBHOOK_TIMEOUT_MS: u64 = 5000;
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn payments_incoming_webhook_flow(
|
||||
async fn payments_incoming_webhook_flow<W: api::OutgoingWebhookType>(
|
||||
state: AppState,
|
||||
merchant_account: storage::MerchantAccount,
|
||||
webhook_details: api::IncomingWebhookDetails,
|
||||
@ -71,7 +71,7 @@ async fn payments_incoming_webhook_flow(
|
||||
.into_report()
|
||||
.change_context(errors::WebhooksFlowError::PaymentsCoreFailed)?;
|
||||
|
||||
create_event_and_trigger_outgoing_webhook(
|
||||
create_event_and_trigger_outgoing_webhook::<W>(
|
||||
state,
|
||||
merchant_account,
|
||||
event_type,
|
||||
@ -91,7 +91,7 @@ async fn payments_incoming_webhook_flow(
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn refunds_incoming_webhook_flow(
|
||||
async fn refunds_incoming_webhook_flow<W: api::OutgoingWebhookType>(
|
||||
state: AppState,
|
||||
merchant_account: storage::MerchantAccount,
|
||||
webhook_details: api::IncomingWebhookDetails,
|
||||
@ -154,7 +154,7 @@ async fn refunds_incoming_webhook_flow(
|
||||
.into_report()
|
||||
.change_context(errors::WebhooksFlowError::RefundsCoreFailed)?;
|
||||
let refund_response: api_models::refunds::RefundResponse = updated_refund.foreign_into();
|
||||
create_event_and_trigger_outgoing_webhook(
|
||||
create_event_and_trigger_outgoing_webhook::<W>(
|
||||
state,
|
||||
merchant_account,
|
||||
event_type,
|
||||
@ -170,7 +170,7 @@ async fn refunds_incoming_webhook_flow(
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[instrument(skip_all)]
|
||||
async fn create_event_and_trigger_outgoing_webhook(
|
||||
async fn create_event_and_trigger_outgoing_webhook<W: api::OutgoingWebhookType>(
|
||||
state: AppState,
|
||||
merchant_account: storage::MerchantAccount,
|
||||
event_type: enums::EventType,
|
||||
@ -211,7 +211,8 @@ async fn create_event_and_trigger_outgoing_webhook(
|
||||
|
||||
arbiter.spawn(async move {
|
||||
let result =
|
||||
trigger_webhook_to_merchant(merchant_account, outgoing_webhook, state.store).await;
|
||||
trigger_webhook_to_merchant::<W>(merchant_account, outgoing_webhook, state.store)
|
||||
.await;
|
||||
|
||||
if let Err(e) = result {
|
||||
logger::error!(?e);
|
||||
@ -222,7 +223,7 @@ async fn create_event_and_trigger_outgoing_webhook(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn trigger_webhook_to_merchant(
|
||||
async fn trigger_webhook_to_merchant<W: api::OutgoingWebhookType>(
|
||||
merchant_account: storage::MerchantAccount,
|
||||
webhook: api::OutgoingWebhook,
|
||||
_db: Box<dyn StorageInterface>,
|
||||
@ -243,10 +244,12 @@ async fn trigger_webhook_to_merchant(
|
||||
.change_context(errors::WebhooksFlowError::MerchantWebhookURLNotConfigured)
|
||||
.map(ExposeInterface::expose)?;
|
||||
|
||||
let transformed_outgoing_webhook = W::from(webhook);
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.post(&webhook_url)
|
||||
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
||||
.json(&webhook)
|
||||
.json(&transformed_outgoing_webhook)
|
||||
.timeout(core::time::Duration::from_millis(
|
||||
OUTGOING_WEBHOOK_TIMEOUT_MS,
|
||||
))
|
||||
@ -272,7 +275,7 @@ async fn trigger_webhook_to_merchant(
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn webhooks_core(
|
||||
pub async fn webhooks_core<W: api::OutgoingWebhookType>(
|
||||
state: &AppState,
|
||||
req: &actix_web::HttpRequest,
|
||||
merchant_account: storage::MerchantAccount,
|
||||
@ -352,7 +355,7 @@ pub async fn webhooks_core(
|
||||
|
||||
let flow_type: api::WebhookFlow = event_type.to_owned().into();
|
||||
match flow_type {
|
||||
api::WebhookFlow::Payment => payments_incoming_webhook_flow(
|
||||
api::WebhookFlow::Payment => payments_incoming_webhook_flow::<W>(
|
||||
state.clone(),
|
||||
merchant_account,
|
||||
webhook_details,
|
||||
@ -362,7 +365,7 @@ pub async fn webhooks_core(
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Incoming webhook flow for payments failed")?,
|
||||
|
||||
api::WebhookFlow::Refund => refunds_incoming_webhook_flow(
|
||||
api::WebhookFlow::Refund => refunds_incoming_webhook_flow::<W>(
|
||||
state.clone(),
|
||||
merchant_account,
|
||||
webhook_details,
|
||||
|
||||
@ -315,12 +315,18 @@ pub struct Webhooks;
|
||||
#[cfg(feature = "oltp")]
|
||||
impl Webhooks {
|
||||
pub fn server(config: AppState) -> Scope {
|
||||
use api_models::webhooks as webhook_type;
|
||||
|
||||
web::scope("/webhooks")
|
||||
.app_data(web::Data::new(config))
|
||||
.service(
|
||||
web::resource("/{merchant_id}/{connector}")
|
||||
.route(web::post().to(receive_incoming_webhook))
|
||||
.route(web::get().to(receive_incoming_webhook)),
|
||||
.route(
|
||||
web::post().to(receive_incoming_webhook::<webhook_type::OutgoingWebhook>),
|
||||
)
|
||||
.route(
|
||||
web::get().to(receive_incoming_webhook::<webhook_type::OutgoingWebhook>),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,10 +5,11 @@ use super::app::AppState;
|
||||
use crate::{
|
||||
core::webhooks,
|
||||
services::{api, authentication as auth},
|
||||
types::api as api_types,
|
||||
};
|
||||
|
||||
#[instrument(skip_all, fields(flow = ?Flow::IncomingWebhookReceive))]
|
||||
pub async fn receive_incoming_webhook(
|
||||
pub async fn receive_incoming_webhook<W: api_types::OutgoingWebhookType>(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
body: web::Bytes,
|
||||
@ -21,7 +22,7 @@ pub async fn receive_incoming_webhook(
|
||||
&req,
|
||||
body,
|
||||
|state, merchant_account, body| {
|
||||
webhooks::webhooks_core(state, &req, merchant_account, &connector_name, body)
|
||||
webhooks::webhooks_core::<W>(state, &req, merchant_account, &connector_name, body)
|
||||
},
|
||||
&auth::MerchantIdAuth(merchant_id),
|
||||
)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
pub use api_models::webhooks::{
|
||||
IncomingWebhookDetails, IncomingWebhookEvent, IncomingWebhookRequestDetails,
|
||||
MerchantWebhookConfig, OutgoingWebhook, OutgoingWebhookContent, WebhookFlow,
|
||||
MerchantWebhookConfig, OutgoingWebhook, OutgoingWebhookContent, OutgoingWebhookType,
|
||||
WebhookFlow,
|
||||
};
|
||||
use error_stack::ResultExt;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user