| 65 |
65 |
|
Ok(false) => {} // first time — proceed
|
| 66 |
66 |
|
}
|
| 67 |
67 |
|
|
| 68 |
|
- |
// Route by event type
|
| 69 |
|
- |
if thin.event_type.starts_with("v2.core.account") {
|
| 70 |
|
- |
if let Err(e) = handle_account_thin_event(&state, stripe.as_ref(), &thin).await {
|
| 71 |
|
- |
// Not marked processed, so returning the error makes Stripe redeliver
|
| 72 |
|
- |
// (retries for up to 3 days) and the handler re-runs.
|
| 73 |
|
- |
tracing::warn!(event_id = %thin.id, error = ?e, "v2 event processing failed; will be redelivered");
|
| 74 |
|
- |
return Err(e);
|
| 75 |
|
- |
}
|
| 76 |
|
- |
// Succeeded — record it so a redelivery short-circuits.
|
| 77 |
|
- |
if let Err(e) = db::webhook_events::mark_event_processed(&state.db, &thin.id).await {
|
| 78 |
|
- |
tracing::error!(event_id = %thin.id, error = ?e, "failed to record processed v2 event; returning 503 for redelivery");
|
|
68 |
+ |
if let Err(e) = process_v2_thin_event(&state, stripe.as_ref(), &thin).await {
|
|
69 |
+ |
// Persist to the local retry queue (backoff + dead-letter WAM via the
|
|
70 |
+ |
// scheduler), matching v1's failure path rather than relying solely on
|
|
71 |
+ |
// Stripe's redelivery window. Not marked processed, so a Stripe
|
|
72 |
+ |
// redelivery also still re-runs the idempotent handler.
|
|
73 |
+ |
tracing::warn!(event_id = %thin.id, error = ?e, "v2 event processing failed; queueing for retry");
|
|
74 |
+ |
if let Err(queue_err) = db::webhook_events::insert_failed_event(
|
|
75 |
+ |
&state.db,
|
|
76 |
+ |
"stripe_v2",
|
|
77 |
+ |
&thin.event_type,
|
|
78 |
+ |
payload,
|
|
79 |
+ |
Some(signature),
|
|
80 |
+ |
&format!("{e:?}"),
|
|
81 |
+ |
)
|
|
82 |
+ |
.await
|
|
83 |
+ |
{
|
|
84 |
+ |
tracing::error!(event_id = %thin.id, error = ?queue_err, "failed to queue v2 event for retry; returning 503 for Stripe redelivery");
|
| 79 |
85 |
|
return Ok(StatusCode::SERVICE_UNAVAILABLE);
|
| 80 |
86 |
|
}
|
| 81 |
|
- |
} else {
|
| 82 |
|
- |
tracing::debug!(event_type = %thin.event_type, "unhandled v2 event type");
|
|
87 |
+ |
return Ok(StatusCode::OK);
|
|
88 |
+ |
}
|
|
89 |
+ |
|
|
90 |
+ |
// Succeeded — record it so a redelivery short-circuits.
|
|
91 |
+ |
if let Err(e) = db::webhook_events::mark_event_processed(&state.db, &thin.id).await {
|
|
92 |
+ |
tracing::error!(event_id = %thin.id, error = ?e, "failed to record processed v2 event; returning 503 for redelivery");
|
|
93 |
+ |
return Ok(StatusCode::SERVICE_UNAVAILABLE);
|
| 83 |
94 |
|
}
|
| 84 |
95 |
|
|
| 85 |
96 |
|
Ok(StatusCode::OK)
|
| 86 |
97 |
|
}
|
| 87 |
98 |
|
|
|
99 |
+ |
/// Route a verified v2 thin event to its handler. Shared by the live webhook and
|
|
100 |
+ |
/// the scheduler retry worker (which re-parses the stored payload — the
|
|
101 |
+ |
/// signature was already verified when the event was first received).
|
|
102 |
+ |
pub(crate) async fn process_v2_thin_event(
|
|
103 |
+ |
state: &AppState,
|
|
104 |
+ |
stripe: &dyn payments::PaymentProvider,
|
|
105 |
+ |
thin: &ThinEvent,
|
|
106 |
+ |
) -> Result<()> {
|
|
107 |
+ |
if thin.event_type.starts_with("v2.core.account") {
|
|
108 |
+ |
handle_account_thin_event(state, stripe, thin).await
|
|
109 |
+ |
} else {
|
|
110 |
+ |
tracing::debug!(event_type = %thin.event_type, "unhandled v2 event type");
|
|
111 |
+ |
Ok(())
|
|
112 |
+ |
}
|
|
113 |
+ |
}
|
|
114 |
+ |
|
| 88 |
115 |
|
/// Fetch the full account object and delegate to the shared account-updated handler.
|
| 89 |
116 |
|
async fn handle_account_thin_event(
|
| 90 |
117 |
|
state: &AppState,
|