1use std::str::FromStr;
2use std::sync::Arc;
3use std::time::Duration;
4
5use anyhow::{anyhow, bail, Context};
6use axum::{
7 extract::{self, Query},
8 routing::{get, post},
9 Extension, Json, Router,
10};
11use chrono::{DateTime, SecondsFormat, Utc};
12use reqwest::StatusCode;
13use sea_orm::ActiveValue;
14use serde::{Deserialize, Serialize};
15use stripe::{
16 BillingPortalSession, CheckoutSession, CreateBillingPortalSession,
17 CreateBillingPortalSessionFlowData, CreateBillingPortalSessionFlowDataAfterCompletion,
18 CreateBillingPortalSessionFlowDataAfterCompletionRedirect,
19 CreateBillingPortalSessionFlowDataType, CreateCheckoutSession, CreateCheckoutSessionLineItems,
20 CreateCustomer, Customer, CustomerId, EventObject, EventType, Expandable, ListEvents,
21 Subscription, SubscriptionId, SubscriptionStatus,
22};
23use util::ResultExt;
24
25use crate::db::billing_subscription::{self, StripeSubscriptionStatus};
26use crate::db::{
27 billing_customer, BillingSubscriptionId, CreateBillingCustomerParams,
28 CreateBillingSubscriptionParams, CreateProcessedStripeEventParams, UpdateBillingCustomerParams,
29 UpdateBillingSubscriptionParams,
30};
31use crate::llm::db::LlmDatabase;
32use crate::llm::MONTHLY_SPENDING_LIMIT_IN_CENTS;
33use crate::rpc::ResultExt as _;
34use crate::{AppState, Error, Result};
35
36pub fn router() -> Router {
37 Router::new()
38 .route(
39 "/billing/subscriptions",
40 get(list_billing_subscriptions).post(create_billing_subscription),
41 )
42 .route(
43 "/billing/subscriptions/manage",
44 post(manage_billing_subscription),
45 )
46}
47
48#[derive(Debug, Deserialize)]
49struct ListBillingSubscriptionsParams {
50 github_user_id: i32,
51}
52
53#[derive(Debug, Serialize)]
54struct BillingSubscriptionJson {
55 id: BillingSubscriptionId,
56 name: String,
57 status: StripeSubscriptionStatus,
58 cancel_at: Option<String>,
59 /// Whether this subscription can be canceled.
60 is_cancelable: bool,
61}
62
63#[derive(Debug, Serialize)]
64struct ListBillingSubscriptionsResponse {
65 subscriptions: Vec<BillingSubscriptionJson>,
66}
67
68async fn list_billing_subscriptions(
69 Extension(app): Extension<Arc<AppState>>,
70 Query(params): Query<ListBillingSubscriptionsParams>,
71) -> Result<Json<ListBillingSubscriptionsResponse>> {
72 let user = app
73 .db
74 .get_user_by_github_user_id(params.github_user_id)
75 .await?
76 .ok_or_else(|| anyhow!("user not found"))?;
77
78 let subscriptions = app.db.get_billing_subscriptions(user.id).await?;
79
80 Ok(Json(ListBillingSubscriptionsResponse {
81 subscriptions: subscriptions
82 .into_iter()
83 .map(|subscription| BillingSubscriptionJson {
84 id: subscription.id,
85 name: "Zed LLM Usage".to_string(),
86 status: subscription.stripe_subscription_status,
87 cancel_at: subscription.stripe_cancel_at.map(|cancel_at| {
88 cancel_at
89 .and_utc()
90 .to_rfc3339_opts(SecondsFormat::Millis, true)
91 }),
92 is_cancelable: subscription.stripe_subscription_status.is_cancelable()
93 && subscription.stripe_cancel_at.is_none(),
94 })
95 .collect(),
96 }))
97}
98
99#[derive(Debug, Deserialize)]
100struct CreateBillingSubscriptionBody {
101 github_user_id: i32,
102}
103
104#[derive(Debug, Serialize)]
105struct CreateBillingSubscriptionResponse {
106 checkout_session_url: String,
107}
108
109/// Initiates a Stripe Checkout session for creating a billing subscription.
110async fn create_billing_subscription(
111 Extension(app): Extension<Arc<AppState>>,
112 extract::Json(body): extract::Json<CreateBillingSubscriptionBody>,
113) -> Result<Json<CreateBillingSubscriptionResponse>> {
114 let user = app
115 .db
116 .get_user_by_github_user_id(body.github_user_id)
117 .await?
118 .ok_or_else(|| anyhow!("user not found"))?;
119
120 let Some((stripe_client, stripe_price_id)) = app
121 .stripe_client
122 .clone()
123 .zip(app.config.stripe_llm_usage_price_id.clone())
124 else {
125 log::error!("failed to retrieve Stripe client or price ID");
126 Err(Error::http(
127 StatusCode::NOT_IMPLEMENTED,
128 "not supported".into(),
129 ))?
130 };
131
132 let customer_id =
133 if let Some(existing_customer) = app.db.get_billing_customer_by_user_id(user.id).await? {
134 CustomerId::from_str(&existing_customer.stripe_customer_id)
135 .context("failed to parse customer ID")?
136 } else {
137 let customer = Customer::create(
138 &stripe_client,
139 CreateCustomer {
140 email: user.email_address.as_deref(),
141 ..Default::default()
142 },
143 )
144 .await?;
145
146 customer.id
147 };
148
149 let checkout_session = {
150 let mut params = CreateCheckoutSession::new();
151 params.mode = Some(stripe::CheckoutSessionMode::Subscription);
152 params.customer = Some(customer_id);
153 params.client_reference_id = Some(user.github_login.as_str());
154 params.line_items = Some(vec![CreateCheckoutSessionLineItems {
155 price: Some(stripe_price_id.to_string()),
156 quantity: Some(0),
157 ..Default::default()
158 }]);
159 let success_url = format!("{}/account", app.config.zed_dot_dev_url());
160 params.success_url = Some(&success_url);
161
162 CheckoutSession::create(&stripe_client, params).await?
163 };
164
165 Ok(Json(CreateBillingSubscriptionResponse {
166 checkout_session_url: checkout_session
167 .url
168 .ok_or_else(|| anyhow!("no checkout session URL"))?,
169 }))
170}
171
172#[derive(Debug, PartialEq, Deserialize)]
173#[serde(rename_all = "snake_case")]
174enum ManageSubscriptionIntent {
175 /// The user intends to cancel their subscription.
176 Cancel,
177 /// The user intends to stop the cancellation of their subscription.
178 StopCancellation,
179}
180
181#[derive(Debug, Deserialize)]
182struct ManageBillingSubscriptionBody {
183 github_user_id: i32,
184 intent: ManageSubscriptionIntent,
185 /// The ID of the subscription to manage.
186 subscription_id: BillingSubscriptionId,
187}
188
189#[derive(Debug, Serialize)]
190struct ManageBillingSubscriptionResponse {
191 billing_portal_session_url: Option<String>,
192}
193
194/// Initiates a Stripe customer portal session for managing a billing subscription.
195async fn manage_billing_subscription(
196 Extension(app): Extension<Arc<AppState>>,
197 extract::Json(body): extract::Json<ManageBillingSubscriptionBody>,
198) -> Result<Json<ManageBillingSubscriptionResponse>> {
199 let user = app
200 .db
201 .get_user_by_github_user_id(body.github_user_id)
202 .await?
203 .ok_or_else(|| anyhow!("user not found"))?;
204
205 let Some(stripe_client) = app.stripe_client.clone() else {
206 log::error!("failed to retrieve Stripe client");
207 Err(Error::http(
208 StatusCode::NOT_IMPLEMENTED,
209 "not supported".into(),
210 ))?
211 };
212
213 let customer = app
214 .db
215 .get_billing_customer_by_user_id(user.id)
216 .await?
217 .ok_or_else(|| anyhow!("billing customer not found"))?;
218 let customer_id = CustomerId::from_str(&customer.stripe_customer_id)
219 .context("failed to parse customer ID")?;
220
221 let subscription = app
222 .db
223 .get_billing_subscription_by_id(body.subscription_id)
224 .await?
225 .ok_or_else(|| anyhow!("subscription not found"))?;
226
227 if body.intent == ManageSubscriptionIntent::StopCancellation {
228 let subscription_id = SubscriptionId::from_str(&subscription.stripe_subscription_id)
229 .context("failed to parse subscription ID")?;
230
231 let updated_stripe_subscription = Subscription::update(
232 &stripe_client,
233 &subscription_id,
234 stripe::UpdateSubscription {
235 cancel_at_period_end: Some(false),
236 ..Default::default()
237 },
238 )
239 .await?;
240
241 app.db
242 .update_billing_subscription(
243 subscription.id,
244 &UpdateBillingSubscriptionParams {
245 stripe_cancel_at: ActiveValue::set(
246 updated_stripe_subscription
247 .cancel_at
248 .and_then(|cancel_at| DateTime::from_timestamp(cancel_at, 0))
249 .map(|time| time.naive_utc()),
250 ),
251 ..Default::default()
252 },
253 )
254 .await?;
255
256 return Ok(Json(ManageBillingSubscriptionResponse {
257 billing_portal_session_url: None,
258 }));
259 }
260
261 let flow = match body.intent {
262 ManageSubscriptionIntent::Cancel => CreateBillingPortalSessionFlowData {
263 type_: CreateBillingPortalSessionFlowDataType::SubscriptionCancel,
264 after_completion: Some(CreateBillingPortalSessionFlowDataAfterCompletion {
265 type_: stripe::CreateBillingPortalSessionFlowDataAfterCompletionType::Redirect,
266 redirect: Some(CreateBillingPortalSessionFlowDataAfterCompletionRedirect {
267 return_url: format!("{}/account", app.config.zed_dot_dev_url()),
268 }),
269 ..Default::default()
270 }),
271 subscription_cancel: Some(
272 stripe::CreateBillingPortalSessionFlowDataSubscriptionCancel {
273 subscription: subscription.stripe_subscription_id,
274 retention: None,
275 },
276 ),
277 ..Default::default()
278 },
279 ManageSubscriptionIntent::StopCancellation => unreachable!(),
280 };
281
282 let mut params = CreateBillingPortalSession::new(customer_id);
283 params.flow_data = Some(flow);
284 let return_url = format!("{}/account", app.config.zed_dot_dev_url());
285 params.return_url = Some(&return_url);
286
287 let session = BillingPortalSession::create(&stripe_client, params).await?;
288
289 Ok(Json(ManageBillingSubscriptionResponse {
290 billing_portal_session_url: Some(session.url),
291 }))
292}
293
294/// The amount of time we wait in between each poll of Stripe events.
295///
296/// This value should strike a balance between:
297/// 1. Being short enough that we update quickly when something in Stripe changes
298/// 2. Being long enough that we don't eat into our rate limits.
299///
300/// As a point of reference, the Sequin folks say they have this at **500ms**:
301///
302/// > We poll the Stripe /events endpoint every 500ms per account
303/// >
304/// > — https://blog.sequinstream.com/events-not-webhooks/
305const POLL_EVENTS_INTERVAL: Duration = Duration::from_secs(5);
306
307/// The maximum number of events to return per page.
308///
309/// We set this to 100 (the max) so we have to make fewer requests to Stripe.
310///
311/// > Limit can range between 1 and 100, and the default is 10.
312const EVENTS_LIMIT_PER_PAGE: u64 = 100;
313
314/// The number of pages consisting entirely of already-processed events that we
315/// will see before we stop retrieving events.
316///
317/// This is used to prevent over-fetching the Stripe events API for events we've
318/// already seen and processed.
319const NUMBER_OF_ALREADY_PROCESSED_PAGES_BEFORE_WE_STOP: usize = 4;
320
321/// Polls the Stripe events API periodically to reconcile the records in our
322/// database with the data in Stripe.
323pub fn poll_stripe_events_periodically(app: Arc<AppState>) {
324 let Some(stripe_client) = app.stripe_client.clone() else {
325 log::warn!("failed to retrieve Stripe client");
326 return;
327 };
328
329 let executor = app.executor.clone();
330 executor.spawn_detached({
331 let executor = executor.clone();
332 async move {
333 loop {
334 poll_stripe_events(&app, &stripe_client).await.log_err();
335
336 executor.sleep(POLL_EVENTS_INTERVAL).await;
337 }
338 }
339 });
340}
341
342async fn poll_stripe_events(
343 app: &Arc<AppState>,
344 stripe_client: &stripe::Client,
345) -> anyhow::Result<()> {
346 fn event_type_to_string(event_type: EventType) -> String {
347 // Calling `to_string` on `stripe::EventType` members gives us a quoted string,
348 // so we need to unquote it.
349 event_type.to_string().trim_matches('"').to_string()
350 }
351
352 let event_types = [
353 EventType::CustomerCreated,
354 EventType::CustomerUpdated,
355 EventType::CustomerSubscriptionCreated,
356 EventType::CustomerSubscriptionUpdated,
357 EventType::CustomerSubscriptionPaused,
358 EventType::CustomerSubscriptionResumed,
359 EventType::CustomerSubscriptionDeleted,
360 ]
361 .into_iter()
362 .map(event_type_to_string)
363 .collect::<Vec<_>>();
364
365 let mut pages_of_already_processed_events = 0;
366 let mut unprocessed_events = Vec::new();
367
368 loop {
369 if pages_of_already_processed_events >= NUMBER_OF_ALREADY_PROCESSED_PAGES_BEFORE_WE_STOP {
370 log::info!("saw {pages_of_already_processed_events} pages of already-processed events: stopping event retrieval");
371 break;
372 }
373
374 log::info!("retrieving events from Stripe: {}", event_types.join(", "));
375
376 let mut params = ListEvents::new();
377 params.types = Some(event_types.clone());
378 params.limit = Some(EVENTS_LIMIT_PER_PAGE);
379
380 let events = stripe::Event::list(stripe_client, ¶ms).await?;
381
382 let processed_event_ids = {
383 let event_ids = &events
384 .data
385 .iter()
386 .map(|event| event.id.as_str())
387 .collect::<Vec<_>>();
388
389 app.db
390 .get_processed_stripe_events_by_event_ids(event_ids)
391 .await?
392 .into_iter()
393 .map(|event| event.stripe_event_id)
394 .collect::<Vec<_>>()
395 };
396
397 let mut processed_events_in_page = 0;
398 let events_in_page = events.data.len();
399 for event in events.data {
400 if processed_event_ids.contains(&event.id.to_string()) {
401 processed_events_in_page += 1;
402 log::debug!("Stripe event {} already processed: skipping", event.id);
403 } else {
404 unprocessed_events.push(event);
405 }
406 }
407
408 if processed_events_in_page == events_in_page {
409 pages_of_already_processed_events += 1;
410 }
411
412 if !events.has_more {
413 break;
414 }
415 }
416
417 log::info!(
418 "unprocessed events from Stripe: {}",
419 unprocessed_events.len()
420 );
421
422 // Sort all of the unprocessed events in ascending order, so we can handle them in the order they occurred.
423 unprocessed_events.sort_by(|a, b| a.created.cmp(&b.created).then_with(|| a.id.cmp(&b.id)));
424
425 for event in unprocessed_events {
426 let event_id = event.id.clone();
427 let processed_event_params = CreateProcessedStripeEventParams {
428 stripe_event_id: event.id.to_string(),
429 stripe_event_type: event_type_to_string(event.type_),
430 stripe_event_created_timestamp: event.created,
431 };
432
433 // If the event has happened too far in the past, we don't want to
434 // process it and risk overwriting other more-recent updates.
435 //
436 // 1 hour was chosen arbitrarily. This could be made longer or shorter.
437 let one_hour = Duration::from_secs(60 * 60);
438 let an_hour_ago = Utc::now() - one_hour;
439 if an_hour_ago.timestamp() > event.created {
440 log::info!(
441 "Stripe event {} is more than {one_hour:?} old, marking as processed",
442 event_id
443 );
444 app.db
445 .create_processed_stripe_event(&processed_event_params)
446 .await?;
447
448 return Ok(());
449 }
450
451 let process_result = match event.type_ {
452 EventType::CustomerCreated | EventType::CustomerUpdated => {
453 handle_customer_event(app, stripe_client, event).await
454 }
455 EventType::CustomerSubscriptionCreated
456 | EventType::CustomerSubscriptionUpdated
457 | EventType::CustomerSubscriptionPaused
458 | EventType::CustomerSubscriptionResumed
459 | EventType::CustomerSubscriptionDeleted => {
460 handle_customer_subscription_event(app, stripe_client, event).await
461 }
462 _ => Ok(()),
463 };
464
465 if let Some(()) = process_result
466 .with_context(|| format!("failed to process event {event_id} successfully"))
467 .log_err()
468 {
469 app.db
470 .create_processed_stripe_event(&processed_event_params)
471 .await?;
472 }
473 }
474
475 Ok(())
476}
477
478async fn handle_customer_event(
479 app: &Arc<AppState>,
480 _stripe_client: &stripe::Client,
481 event: stripe::Event,
482) -> anyhow::Result<()> {
483 let EventObject::Customer(customer) = event.data.object else {
484 bail!("unexpected event payload for {}", event.id);
485 };
486
487 log::info!("handling Stripe {} event: {}", event.type_, event.id);
488
489 let Some(email) = customer.email else {
490 log::info!("Stripe customer has no email: skipping");
491 return Ok(());
492 };
493
494 let Some(user) = app.db.get_user_by_email(&email).await? else {
495 log::info!("no user found for email: skipping");
496 return Ok(());
497 };
498
499 if let Some(existing_customer) = app
500 .db
501 .get_billing_customer_by_stripe_customer_id(&customer.id)
502 .await?
503 {
504 app.db
505 .update_billing_customer(
506 existing_customer.id,
507 &UpdateBillingCustomerParams {
508 // For now we just leave the information as-is, as it is not
509 // likely to change.
510 ..Default::default()
511 },
512 )
513 .await?;
514 } else {
515 app.db
516 .create_billing_customer(&CreateBillingCustomerParams {
517 user_id: user.id,
518 stripe_customer_id: customer.id.to_string(),
519 })
520 .await?;
521 }
522
523 Ok(())
524}
525
526async fn handle_customer_subscription_event(
527 app: &Arc<AppState>,
528 stripe_client: &stripe::Client,
529 event: stripe::Event,
530) -> anyhow::Result<()> {
531 let EventObject::Subscription(subscription) = event.data.object else {
532 bail!("unexpected event payload for {}", event.id);
533 };
534
535 log::info!("handling Stripe {} event: {}", event.type_, event.id);
536
537 let billing_customer =
538 find_or_create_billing_customer(app, stripe_client, subscription.customer)
539 .await?
540 .ok_or_else(|| anyhow!("billing customer not found"))?;
541
542 if let Some(existing_subscription) = app
543 .db
544 .get_billing_subscription_by_stripe_subscription_id(&subscription.id)
545 .await?
546 {
547 app.db
548 .update_billing_subscription(
549 existing_subscription.id,
550 &UpdateBillingSubscriptionParams {
551 billing_customer_id: ActiveValue::set(billing_customer.id),
552 stripe_subscription_id: ActiveValue::set(subscription.id.to_string()),
553 stripe_subscription_status: ActiveValue::set(subscription.status.into()),
554 stripe_cancel_at: ActiveValue::set(
555 subscription
556 .cancel_at
557 .and_then(|cancel_at| DateTime::from_timestamp(cancel_at, 0))
558 .map(|time| time.naive_utc()),
559 ),
560 },
561 )
562 .await?;
563 } else {
564 app.db
565 .create_billing_subscription(&CreateBillingSubscriptionParams {
566 billing_customer_id: billing_customer.id,
567 stripe_subscription_id: subscription.id.to_string(),
568 stripe_subscription_status: subscription.status.into(),
569 })
570 .await?;
571 }
572
573 Ok(())
574}
575
576impl From<SubscriptionStatus> for StripeSubscriptionStatus {
577 fn from(value: SubscriptionStatus) -> Self {
578 match value {
579 SubscriptionStatus::Incomplete => Self::Incomplete,
580 SubscriptionStatus::IncompleteExpired => Self::IncompleteExpired,
581 SubscriptionStatus::Trialing => Self::Trialing,
582 SubscriptionStatus::Active => Self::Active,
583 SubscriptionStatus::PastDue => Self::PastDue,
584 SubscriptionStatus::Canceled => Self::Canceled,
585 SubscriptionStatus::Unpaid => Self::Unpaid,
586 SubscriptionStatus::Paused => Self::Paused,
587 }
588 }
589}
590
591/// Finds or creates a billing customer using the provided customer.
592async fn find_or_create_billing_customer(
593 app: &Arc<AppState>,
594 stripe_client: &stripe::Client,
595 customer_or_id: Expandable<Customer>,
596) -> anyhow::Result<Option<billing_customer::Model>> {
597 let customer_id = match &customer_or_id {
598 Expandable::Id(id) => id,
599 Expandable::Object(customer) => customer.id.as_ref(),
600 };
601
602 // If we already have a billing customer record associated with the Stripe customer,
603 // there's nothing more we need to do.
604 if let Some(billing_customer) = app
605 .db
606 .get_billing_customer_by_stripe_customer_id(customer_id)
607 .await?
608 {
609 return Ok(Some(billing_customer));
610 }
611
612 // If all we have is a customer ID, resolve it to a full customer record by
613 // hitting the Stripe API.
614 let customer = match customer_or_id {
615 Expandable::Id(id) => Customer::retrieve(stripe_client, &id, &[]).await?,
616 Expandable::Object(customer) => *customer,
617 };
618
619 let Some(email) = customer.email else {
620 return Ok(None);
621 };
622
623 let Some(user) = app.db.get_user_by_email(&email).await? else {
624 return Ok(None);
625 };
626
627 let billing_customer = app
628 .db
629 .create_billing_customer(&CreateBillingCustomerParams {
630 user_id: user.id,
631 stripe_customer_id: customer.id.to_string(),
632 })
633 .await?;
634
635 Ok(Some(billing_customer))
636}
637
638const SYNC_LLM_USAGE_WITH_STRIPE_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60);
639
640pub fn sync_llm_usage_with_stripe_periodically(app: Arc<AppState>, llm_db: LlmDatabase) {
641 let Some(stripe_client) = app.stripe_client.clone() else {
642 log::warn!("failed to retrieve Stripe client");
643 return;
644 };
645 let Some(stripe_llm_usage_price_id) = app.config.stripe_llm_usage_price_id.clone() else {
646 log::warn!("failed to retrieve Stripe LLM usage price ID");
647 return;
648 };
649
650 let executor = app.executor.clone();
651 executor.spawn_detached({
652 let executor = executor.clone();
653 async move {
654 loop {
655 sync_with_stripe(
656 &app,
657 &llm_db,
658 &stripe_client,
659 stripe_llm_usage_price_id.clone(),
660 )
661 .await
662 .trace_err();
663
664 executor.sleep(SYNC_LLM_USAGE_WITH_STRIPE_INTERVAL).await;
665 }
666 }
667 });
668}
669
670async fn sync_with_stripe(
671 app: &Arc<AppState>,
672 llm_db: &LlmDatabase,
673 stripe_client: &stripe::Client,
674 stripe_llm_usage_price_id: Arc<str>,
675) -> anyhow::Result<()> {
676 let subscriptions = app.db.get_active_billing_subscriptions().await?;
677
678 for (customer, subscription) in subscriptions {
679 update_stripe_subscription(
680 llm_db,
681 stripe_client,
682 &stripe_llm_usage_price_id,
683 customer,
684 subscription,
685 )
686 .await
687 .log_err();
688 }
689
690 Ok(())
691}
692
693async fn update_stripe_subscription(
694 llm_db: &LlmDatabase,
695 stripe_client: &stripe::Client,
696 stripe_llm_usage_price_id: &Arc<str>,
697 customer: billing_customer::Model,
698 subscription: billing_subscription::Model,
699) -> Result<(), anyhow::Error> {
700 let monthly_spending = llm_db
701 .get_user_spending_for_month(customer.user_id, Utc::now())
702 .await?;
703 let subscription_id = SubscriptionId::from_str(&subscription.stripe_subscription_id)
704 .context("failed to parse subscription ID")?;
705
706 let monthly_spending_over_free_tier =
707 monthly_spending.saturating_sub(MONTHLY_SPENDING_LIMIT_IN_CENTS);
708
709 let new_quantity = (monthly_spending_over_free_tier as f32 / 100.).ceil();
710 Subscription::update(
711 stripe_client,
712 &subscription_id,
713 stripe::UpdateSubscription {
714 items: Some(vec![stripe::UpdateSubscriptionItems {
715 // TODO: Do we need to send up the `id` if a subscription item
716 // with this price already exists, or will Stripe take care of
717 // it?
718 id: None,
719 price: Some(stripe_llm_usage_price_id.to_string()),
720 quantity: Some(new_quantity as u64),
721 ..Default::default()
722 }]),
723 ..Default::default()
724 },
725 )
726 .await?;
727 Ok(())
728}