chore: move billing client (#618)

* chore: move billing client

* chore: cargo fmt
This commit is contained in:
Zack 2024-06-13 13:19:40 +08:00 committed by GitHub
parent ee16f428c9
commit 6471831561
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 271 additions and 0 deletions

12
Cargo.lock generated
View File

@ -1212,6 +1212,18 @@ dependencies = [
"serde",
]
[[package]]
name = "billing"
version = "0.1.0"
dependencies = [
"client-api",
"reqwest 0.11.27",
"serde",
"serde_json",
"shared-entity",
"tokio",
]
[[package]]
name = "bincode"
version = "1.3.3"

View File

@ -160,6 +160,7 @@ members = [
# xtask
"xtask",
"libs/tonic-proto",
"libs/billing",
]
[workspace.dependencies]

13
libs/billing/Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "billing"
version = "0.1.0"
edition = "2021"
[dependencies]
client-api = { path = "../client-api" }
shared-entity = { path = "../shared-entity" }
reqwest = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }

View File

@ -0,0 +1,85 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum RecurringInterval {
Month,
Year,
}
impl RecurringInterval {
pub fn as_str(&self) -> &str {
match self {
RecurringInterval::Month => "month",
RecurringInterval::Year => "year",
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum SubscriptionPlan {
Pro,
Team,
}
impl SubscriptionPlan {
pub fn as_str(&self) -> &str {
match self {
SubscriptionPlan::Pro => "pro",
SubscriptionPlan::Team => "team",
}
}
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "snake_case")]
#[repr(i16)]
pub enum WorkspaceSubscriptionPlan {
Unknown = -1,
Free = 0,
Pro = 1,
Team = 2,
}
#[derive(Copy, Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum SubscriptionStatus {
Active,
Canceled,
Incomplete,
IncompleteExpired,
PastDue,
Paused,
Trialing,
Unpaid,
}
#[derive(Deserialize, Debug)]
pub struct WorkspaceSubscriptionStatus {
pub workspace_id: String,
pub workspace_plan: WorkspaceSubscriptionPlan,
pub recurring_interval: RecurringInterval,
pub subscription_status: SubscriptionStatus,
pub subscription_quantity: u64,
pub canceled_at: Option<i64>,
}
#[derive(Deserialize, Debug)]
pub struct WorkspaceUsage {
pub member_count: usize,
pub member_count_limit: usize,
pub total_blob_bytes: usize,
pub total_blob_bytes_limit: usize,
// TODO(AI):
// pub ai_responses: String,
// pub ai_responses_limit: String,
}
#[derive(Deserialize)]
pub struct WorkspaceUsageLimit {
pub total_blob_size: usize,
pub single_blob_size: usize,
pub member_count: usize,
}

160
libs/billing/src/lib.rs Normal file
View File

@ -0,0 +1,160 @@
pub mod entities;
use crate::entities::WorkspaceUsageLimit;
use client_api::error::AppResponseError;
use entities::{RecurringInterval, SubscriptionPlan, WorkspaceSubscriptionStatus, WorkspaceUsage};
use reqwest::Method;
use serde_json::json;
use shared_entity::response::AppResponse;
pub struct BillingClient<'a> {
billing_base_url: String,
client: &'a client_api::Client,
}
impl<'a> From<&'a client_api::Client> for BillingClient<'a> {
fn from(client: &'a client_api::Client) -> Self {
Self {
billing_base_url: client.base_url.clone(),
client,
}
}
}
impl BillingClient<'_> {
pub fn set_billing_base_url(&mut self, billing_base_url: String) {
self.billing_base_url = billing_base_url;
}
pub async fn customer_id(&self) -> Result<String, AppResponseError> {
let url = format!("{}/billing/api/v1/customer-id", &self.billing_base_url,);
let resp = self
.client
.http_client_with_auth(Method::GET, &url)
.await?
.send()
.await?;
AppResponse::<String>::from_response(resp)
.await?
.into_data()
}
pub async fn create_subscription(
&self,
workspace_id: &str,
recurring_interval: RecurringInterval,
workspace_subscription_plan: SubscriptionPlan,
success_url: &str,
) -> Result<String, AppResponseError> {
let url = format!(
"{}/billing/api/v1/subscription-link",
&self.billing_base_url,
);
let resp = self
.client
.http_client_with_auth(Method::GET, &url)
.await?
.query(&[
("workspace_id", workspace_id),
("recurring_interval", recurring_interval.as_str()),
(
"workspace_subscription_plan",
&workspace_subscription_plan.as_str(),
),
("success_url", success_url),
])
.send()
.await?;
AppResponse::<String>::from_response(resp)
.await?
.into_data()
}
pub async fn cancel_subscription(&self, workspace_id: &str) -> Result<(), AppResponseError> {
let url = format!(
"{}/billing/api/v1/cancel-subscription",
&self.billing_base_url,
);
let resp = self
.client
.http_client_with_auth(Method::POST, &url)
.await?
.json(&json!({ "workspace_id": workspace_id }))
.send()
.await?;
AppResponse::<()>::from_response(resp).await?.into_error()
}
pub async fn list_subscription(
&self,
) -> Result<Vec<WorkspaceSubscriptionStatus>, AppResponseError> {
let url = format!(
"{}/billing/api/v1/subscription-status",
&self.billing_base_url
);
let resp = self
.client
.http_client_with_auth(Method::GET, &url)
.await?
.send()
.await?;
AppResponse::<Vec<WorkspaceSubscriptionStatus>>::from_response(resp)
.await?
.into_data()
}
pub async fn get_workspace_usage(
&self,
workspace_id: &str,
) -> Result<WorkspaceUsage, AppResponseError> {
let num_members = self.client.get_workspace_members(workspace_id).await?.len();
let limits = get_workspace_limits(self.client, workspace_id).await?;
let doc_usage = self.client.get_workspace_usage(workspace_id).await?;
let workspace_usage = WorkspaceUsage {
member_count: num_members,
member_count_limit: limits.member_count,
total_blob_bytes: doc_usage.consumed_capacity as _,
total_blob_bytes_limit: limits.total_blob_size,
};
Ok(workspace_usage)
}
pub async fn get_portal_session_link(&self) -> Result<String, AppResponseError> {
let url = format!(
"{}/billing/api/v1/portal-session-link",
&self.billing_base_url,
);
let portal_url = self
.client
.http_client_with_auth(Method::GET, &url)
.await?
.send()
.await?
.error_for_status()?
.json::<AppResponse<String>>()
.await?
.into_data()?;
Ok(portal_url)
}
}
async fn get_workspace_limits(
client: &client_api::Client,
workspace_id: &str,
) -> Result<WorkspaceUsageLimit, AppResponseError> {
let url = format!("{}/api/workspace/{}/limit", &client.base_url, workspace_id);
client
.http_client_with_auth(Method::GET, &url)
.await?
.send()
.await?
.error_for_status()?
.json::<AppResponse<WorkspaceUsageLimit>>()
.await?
.into_data()
}