feat(compatibility): add webhook support for stripe compatibility (#710)

This commit is contained in:
Abhishek
2023-03-05 18:58:04 +05:30
committed by GitHub
parent 3bdc9b0a2f
commit 7916050450
9 changed files with 112 additions and 25 deletions

View File

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

View File

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

View File

@ -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!({})),
})
}
}
}

View 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()),
}
}
}

View File

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

View File

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

View File

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

View File

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