Skip to main content

rustauth_stripe/stripe_api/
paginated_list.rs

1use std::future::Future;
2
3use serde_json::{json, Value};
4
5use super::{StripeApiError, StripeClient};
6
7/// Stripe list endpoints default to 10 items; billing state decisions must scan
8/// the full customer history, not only the first page.
9pub(crate) const STRIPE_LIST_PAGE_LIMIT: u64 = 100;
10/// Safety cap to avoid unbounded loops if Stripe keeps returning `has_more`.
11const MAX_STRIPE_LIST_PAGES: usize = 100;
12
13impl StripeClient {
14    /// List every subscription page for `params` and return a single merged list object.
15    pub async fn list_subscriptions_all(&self, mut params: Value) -> Result<Value, StripeApiError> {
16        self.paginate_list(
17            |page_params| self.list_subscriptions(page_params),
18            &mut params,
19        )
20        .await
21    }
22
23    /// List every subscription schedule page for `params` and return a single merged list object.
24    pub async fn list_subscription_schedules_all(
25        &self,
26        mut params: Value,
27    ) -> Result<Value, StripeApiError> {
28        self.paginate_list(
29            |page_params| self.list_subscription_schedules(page_params),
30            &mut params,
31        )
32        .await
33    }
34
35    /// List every customer page for `params` and return a single merged list object.
36    pub async fn list_customers_all(&self, mut params: Value) -> Result<Value, StripeApiError> {
37        self.paginate_list(|page_params| self.list_customers(page_params), &mut params)
38            .await
39    }
40
41    /// Walk customer search pages until `predicate` matches or the search is exhausted.
42    pub async fn find_customer_from_search<F>(
43        &self,
44        query: &str,
45        mut predicate: F,
46    ) -> Result<Option<Value>, StripeApiError>
47    where
48        F: FnMut(&Value) -> bool,
49    {
50        let mut page_token: Option<String> = None;
51        let mut pages = 0usize;
52        loop {
53            if pages >= MAX_STRIPE_LIST_PAGES {
54                return Ok(None);
55            }
56            pages += 1;
57            let search_result = self
58                .search_customers_page(query, page_token.as_deref())
59                .await?;
60            if let Some(found) = search_result
61                .get("data")
62                .and_then(Value::as_array)
63                .and_then(|customers| {
64                    customers
65                        .iter()
66                        .find(|customer| predicate(customer))
67                        .cloned()
68                })
69            {
70                return Ok(Some(found));
71            }
72            page_token = search_result
73                .get("next_page")
74                .and_then(Value::as_str)
75                .map(str::to_owned);
76            if page_token.is_none() {
77                return Ok(None);
78            }
79        }
80    }
81
82    /// Walk customer list pages until `predicate` matches or the list is exhausted.
83    pub async fn find_customer<F>(
84        &self,
85        mut params: Value,
86        mut predicate: F,
87    ) -> Result<Option<Value>, StripeApiError>
88    where
89        F: FnMut(&Value) -> bool,
90    {
91        let mut pages = 0usize;
92        loop {
93            if pages >= MAX_STRIPE_LIST_PAGES {
94                return Ok(None);
95            }
96            pages += 1;
97            set_list_page_params(&mut params);
98            let page = self.list_customers(params.clone()).await?;
99            if let Some(found) = page
100                .get("data")
101                .and_then(Value::as_array)
102                .and_then(|customers| {
103                    customers
104                        .iter()
105                        .find(|customer| predicate(customer))
106                        .cloned()
107                })
108            {
109                return Ok(Some(found));
110            }
111            if !stripe_list_has_more(&page) {
112                return Ok(None);
113            }
114            let Some(last_id) = last_list_item_id(&page) else {
115                return Ok(None);
116            };
117            set_starting_after(&mut params, last_id);
118        }
119    }
120
121    /// Walk subscription list pages until `predicate` matches or the list is exhausted.
122    pub async fn find_subscription<F>(
123        &self,
124        mut params: Value,
125        mut predicate: F,
126    ) -> Result<Option<Value>, StripeApiError>
127    where
128        F: FnMut(&Value) -> bool,
129    {
130        let mut pages = 0usize;
131        loop {
132            if pages >= MAX_STRIPE_LIST_PAGES {
133                return Ok(None);
134            }
135            pages += 1;
136            set_list_page_params(&mut params);
137            let page = self.list_subscriptions(params.clone()).await?;
138            if let Some(found) = page
139                .get("data")
140                .and_then(Value::as_array)
141                .and_then(|subscriptions| subscriptions.iter().find(|sub| predicate(sub)).cloned())
142            {
143                return Ok(Some(found));
144            }
145            if !stripe_list_has_more(&page) {
146                return Ok(None);
147            }
148            let Some(last_id) = last_list_item_id(&page) else {
149                return Ok(None);
150            };
151            set_starting_after(&mut params, last_id);
152        }
153    }
154
155    /// Walk subscription schedule list pages until `predicate` matches or the list is exhausted.
156    pub async fn find_subscription_schedule<F>(
157        &self,
158        mut params: Value,
159        mut predicate: F,
160    ) -> Result<Option<Value>, StripeApiError>
161    where
162        F: FnMut(&Value) -> bool,
163    {
164        let mut pages = 0usize;
165        loop {
166            if pages >= MAX_STRIPE_LIST_PAGES {
167                return Ok(None);
168            }
169            pages += 1;
170            set_list_page_params(&mut params);
171            let page = self.list_subscription_schedules(params.clone()).await?;
172            if let Some(found) = page
173                .get("data")
174                .and_then(Value::as_array)
175                .and_then(|schedules| {
176                    schedules
177                        .iter()
178                        .find(|schedule| predicate(schedule))
179                        .cloned()
180                })
181            {
182                return Ok(Some(found));
183            }
184            if !stripe_list_has_more(&page) {
185                return Ok(None);
186            }
187            let Some(last_id) = last_list_item_id(&page) else {
188                return Ok(None);
189            };
190            set_starting_after(&mut params, last_id);
191        }
192    }
193
194    async fn paginate_list<F, Fut>(
195        &self,
196        fetch_page: F,
197        params: &mut Value,
198    ) -> Result<Value, StripeApiError>
199    where
200        F: Fn(Value) -> Fut,
201        Fut: Future<Output = Result<Value, StripeApiError>>,
202    {
203        let mut merged = Vec::new();
204        let mut pages = 0usize;
205        loop {
206            if pages >= MAX_STRIPE_LIST_PAGES {
207                break;
208            }
209            pages += 1;
210            set_list_page_params(params);
211            let page = fetch_page(params.clone()).await?;
212            if let Some(data) = page.get("data").and_then(Value::as_array) {
213                merged.extend(data.iter().cloned());
214            }
215            if !stripe_list_has_more(&page) {
216                break;
217            }
218            let Some(last_id) = last_list_item_id(&page) else {
219                break;
220            };
221            set_starting_after(params, last_id);
222        }
223        Ok(json!({
224            "object": "list",
225            "data": merged,
226            "has_more": false,
227        }))
228    }
229}
230
231fn set_list_page_params(params: &mut Value) {
232    let Some(object) = params.as_object_mut() else {
233        return;
234    };
235    object.insert("limit".to_owned(), json!(STRIPE_LIST_PAGE_LIMIT));
236}
237
238fn set_starting_after(params: &mut Value, id: &str) {
239    let Some(object) = params.as_object_mut() else {
240        return;
241    };
242    object.insert("starting_after".to_owned(), json!(id));
243}
244
245fn stripe_list_has_more(page: &Value) -> bool {
246    page.get("has_more")
247        .and_then(Value::as_bool)
248        .unwrap_or(false)
249}
250
251fn last_list_item_id(page: &Value) -> Option<&str> {
252    page.get("data")?.as_array()?.last()?.get("id")?.as_str()
253}