orb_billing/client/
subscriptions.rs

1// Copyright Materialize, Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License in the LICENSE file at the
6// root of this repository, or online at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use futures_core::Stream;
17use futures_util::stream::TryStreamExt;
18use ordered_float::OrderedFloat;
19use reqwest::Method;
20use serde::{Deserialize, Serialize};
21use serde_enum_str::{Deserialize_enum_str, Serialize_enum_str};
22use time::OffsetDateTime;
23
24use crate::client::customers::{Customer, CustomerId, CustomerResponse};
25use crate::client::marketplaces::ExternalMarketplace;
26use crate::client::plans::{Plan, PlanId};
27use crate::client::Client;
28use crate::config::ListParams;
29use crate::error::Error;
30use crate::util::StrIteratorExt;
31
32const SUBSCRIPTIONS_PATH: [&str; 1] = ["subscriptions"];
33
34/// An Orb subscription.
35#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize)]
36pub struct CreateSubscriptionRequest<'a> {
37    /// An optional user-defined ID for this customer resource, used throughout
38    /// the system as an alias for this customer.
39    #[serde(flatten)]
40    pub customer_id: CustomerId<'a>,
41    /// The plan that the customer should be subscribed to.
42    ///
43    /// The plan determines the pricing and the cadence of the subscription.
44    #[serde(flatten)]
45    pub plan_id: PlanId<'a>,
46    /// The date at which Orb should start billing for the subscription,
47    /// localized ot the customer's timezone.
48    ///
49    /// If `None`, defaults to the current date in the customer's timezone.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    #[serde(with = "time::serde::rfc3339::option")]
52    pub start_date: Option<OffsetDateTime>,
53    /// The name of the external marketplace that the subscription is attached
54    /// to.
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub external_marketplace: Option<SubscriptionExternalMarketplaceRequest<'a>>,
57    /// Whether to align billing periods with the subscription's start date.
58    ///
59    /// If `None`, the value is determined by the plan configuration.
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub align_billing_with_subscription_start_date: Option<bool>,
62    /// The subscription's override minimum amount for the plan.
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub minimum_amount: Option<&'a str>,
65    /// The subscription's override minimum amount for the plan.
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub net_terms: Option<i64>,
68    /// Determines whether issued invoices for this subscription will
69    /// automatically be charged with the saved payment method on the due date.
70    ///
71    /// If `None`, the value is determined by the plan configuration.
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub auto_collection: Option<bool>,
74    /// Determines the default memo on this subscription's invoices.
75    ///
76    /// If `None`, the value is determined by the plan configuration.
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub default_invoice_memo: Option<&'a str>,
79    /// An idempotency key can ensure that if the same request comes in
80    /// multiple times in a 48-hour period, only one makes changes.
81    // NOTE: this is passed in a request header, not the body
82    #[serde(skip_serializing)]
83    pub idempotency_key: Option<&'a str>,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
87pub struct SubscriptionExternalMarketplaceRequest<'a> {
88    /// The kind of the external marketplace.
89    #[serde(rename = "external_marketplace")]
90    pub kind: ExternalMarketplace,
91    /// The ID of the subscription in the external marketplace.
92    #[serde(rename = "external_marketplace_reporting_id")]
93    pub reporting_id: &'a str,
94}
95
96/// An Orb subscription.
97#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
98pub struct Subscription<C = Customer> {
99    /// The Orb-assigned unique identifier for the subscription.
100    pub id: String,
101    /// The customer associated with this subscription.
102    pub customer: C,
103    /// The plan associated with this subscription.
104    pub plan: Plan,
105    /// The date at which Orb starts billing for this subscription.
106    #[serde(with = "time::serde::rfc3339")]
107    pub start_date: OffsetDateTime,
108    /// The date at which Orb stops billing for this subscription.
109    #[serde(with = "time::serde::rfc3339::option")]
110    pub end_date: Option<OffsetDateTime>,
111    /// The status of the subscription.
112    pub status: Option<SubscriptionStatus>,
113    /// The start of the current billing period if the subscription is currently
114    /// active.
115    #[serde(with = "time::serde::rfc3339::option")]
116    pub current_billing_period_start_date: Option<OffsetDateTime>,
117    /// The end of the current billing period if the subscription is currently
118    /// active.
119    #[serde(with = "time::serde::rfc3339::option")]
120    pub current_billing_period_end_date: Option<OffsetDateTime>,
121    /// The current plan phase that is active, if the subscription's plan has
122    /// phases.
123    pub active_plan_phase_order: Option<i64>,
124    /// List of all fixed fee quantities associated with this subscription.
125    pub fixed_fee_quantity_schedule: Vec<SubscriptionFixedFee>,
126    /// Determines the difference between the invoice issue date and the
127    /// date that they are due.
128    ///
129    /// A value of zero indicates that the invoice is due on issue, whereas a
130    /// value of 30 represents that the customer has a month to pay the invoice.
131    pub net_terms: i64,
132    /// Determines whether issued invoices for this subscription will
133    /// automatically be charged with the saved payment method on the due date.
134    ///
135    /// If `None`, the value is determined by the plan configuration.
136    pub auto_collection: bool,
137    /// Determines the default memo on this subscription's invoices.
138    ///
139    /// If `None`, the value is determined by the plan configuration.
140    pub default_invoice_memo: String,
141    /// The time at which the subscription was created.
142    #[serde(with = "time::serde::rfc3339")]
143    pub created_at: OffsetDateTime,
144}
145
146/// The status of an Orb subscription.
147#[non_exhaustive]
148#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize_enum_str, Serialize_enum_str)]
149#[serde(rename_all = "snake_case")]
150pub enum SubscriptionStatus {
151    /// An active subscription.
152    Active,
153    /// A subscription that has ended.
154    Ended,
155    /// A subscription that has not yet started.
156    Upcoming,
157    /// An unknown subscription status.
158    #[serde(other)]
159    Other(String),
160}
161
162/// An entry in [`Subscription::fixed_fee_quantity_schedule`].
163#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
164pub struct SubscriptionFixedFee {
165    /// The date at which the fixed fee starts.
166    #[serde(with = "time::serde::rfc3339")]
167    pub start_date: OffsetDateTime,
168    /// The date at which the fixed fee ends.
169    #[serde(with = "time::serde::rfc3339::option")]
170    pub end_date: Option<OffsetDateTime>,
171    /// The price ID for the fixed fee.
172    pub price_id: String,
173    /// The quantity of the fixed fee.
174    pub quantity: OrderedFloat<f64>,
175}
176
177/// Parameters for a subscription list operation.
178#[derive(Debug, Clone)]
179pub struct SubscriptionListParams<'a> {
180    inner: ListParams,
181    filter: Option<CustomerId<'a>>,
182}
183
184impl<'a> Default for SubscriptionListParams<'a> {
185    fn default() -> SubscriptionListParams<'a> {
186        SubscriptionListParams::DEFAULT
187    }
188}
189
190impl<'a> SubscriptionListParams<'a> {
191    /// The default subscription list parameters.
192    ///
193    /// Exposed as a constant for use in constant evaluation contexts.
194    pub const DEFAULT: SubscriptionListParams<'static> = SubscriptionListParams {
195        inner: ListParams::DEFAULT,
196        filter: None,
197    };
198
199    /// Sets the page size for the list operation.
200    ///
201    /// See [`ListParams::page_size`].
202    pub const fn page_size(mut self, page_size: u64) -> Self {
203        self.inner = self.inner.page_size(page_size);
204        self
205    }
206
207    /// Filters the listing to the specified customer ID.
208    pub const fn customer_id(mut self, filter: CustomerId<'a>) -> Self {
209        self.filter = Some(filter);
210        self
211    }
212}
213
214impl Client {
215    /// Lists subscriptions as configured by `params`.
216    ///
217    /// The underlying API call is paginated. The returned stream will fetch
218    /// additional pages as it is consumed.
219    pub fn list_subscriptions(
220        &self,
221        params: &SubscriptionListParams,
222    ) -> impl Stream<Item = Result<Subscription, Error>> + '_ {
223        let req = self.build_request(Method::GET, SUBSCRIPTIONS_PATH);
224        let req = match params.filter {
225            None => req,
226            Some(CustomerId::Orb(id)) => req.query(&[("customer_id", id)]),
227            Some(CustomerId::External(id)) => req.query(&[("external_customer_id", id)]),
228        };
229        self.stream_paginated_request(&params.inner, req)
230            .try_filter_map(|subscription: Subscription<CustomerResponse>| async move {
231                match subscription.customer {
232                    CustomerResponse::Normal(customer) => Ok(Some(Subscription {
233                        id: subscription.id,
234                        customer,
235                        plan: subscription.plan,
236                        start_date: subscription.start_date,
237                        end_date: subscription.end_date,
238                        status: subscription.status,
239                        current_billing_period_start_date: subscription
240                            .current_billing_period_start_date,
241                        current_billing_period_end_date: subscription
242                            .current_billing_period_end_date,
243                        active_plan_phase_order: subscription.active_plan_phase_order,
244                        fixed_fee_quantity_schedule: subscription.fixed_fee_quantity_schedule,
245                        net_terms: subscription.net_terms,
246                        auto_collection: subscription.auto_collection,
247                        default_invoice_memo: subscription.default_invoice_memo,
248                        created_at: subscription.created_at,
249                    })),
250                    CustomerResponse::Deleted {
251                        id: _,
252                        deleted: true,
253                    } => Ok(None),
254                    CustomerResponse::Deleted { id, deleted: false } => {
255                        Err(Error::UnexpectedResponse {
256                            detail: format!(
257                                "customer {id} used deleted response shape \
258                                but deleted field was `false`"
259                            ),
260                        })
261                    }
262                }
263            })
264    }
265
266    /// Creates a new subscription.
267    pub async fn create_subscription(
268        &self,
269        subscription: &CreateSubscriptionRequest<'_>,
270    ) -> Result<Subscription, Error> {
271        let mut req = self.build_request(Method::POST, SUBSCRIPTIONS_PATH);
272        if let Some(key) = subscription.idempotency_key {
273            req = req.header("Idempotency-Key", key);
274        }
275
276        let req = req.json(subscription);
277        let res = self.send_request(req).await?;
278        Ok(res)
279    }
280
281    /// Gets a subscription by ID.
282    pub async fn get_subscription(&self, id: &str) -> Result<Subscription, Error> {
283        let req = self.build_request(Method::GET, SUBSCRIPTIONS_PATH.chain_one(id));
284        let res = self.send_request(req).await?;
285        Ok(res)
286    }
287
288    // TODO: cancel and unschedule subscriptions.
289}