billing.rs

  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, &params).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}