From 6471831561bf5bd0e3dd481c6961003d00d6caf4 Mon Sep 17 00:00:00 2001 From: Zack Date: Thu, 13 Jun 2024 13:19:40 +0800 Subject: [PATCH] chore: move billing client (#618) * chore: move billing client * chore: cargo fmt --- Cargo.lock | 12 +++ Cargo.toml | 1 + libs/billing/Cargo.toml | 13 +++ libs/billing/src/entities.rs | 85 +++++++++++++++++++ libs/billing/src/lib.rs | 160 +++++++++++++++++++++++++++++++++++ 5 files changed, 271 insertions(+) create mode 100644 libs/billing/Cargo.toml create mode 100644 libs/billing/src/entities.rs create mode 100644 libs/billing/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index b7b76e91..637d6d82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 537892c4..2bd46d00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,6 +160,7 @@ members = [ # xtask "xtask", "libs/tonic-proto", + "libs/billing", ] [workspace.dependencies] diff --git a/libs/billing/Cargo.toml b/libs/billing/Cargo.toml new file mode 100644 index 00000000..c9a3cb94 --- /dev/null +++ b/libs/billing/Cargo.toml @@ -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 } diff --git a/libs/billing/src/entities.rs b/libs/billing/src/entities.rs new file mode 100644 index 00000000..70e313eb --- /dev/null +++ b/libs/billing/src/entities.rs @@ -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, +} + +#[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, +} diff --git a/libs/billing/src/lib.rs b/libs/billing/src/lib.rs new file mode 100644 index 00000000..177ccbd0 --- /dev/null +++ b/libs/billing/src/lib.rs @@ -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 { + 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::::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 { + 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::::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, 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::>::from_response(resp) + .await? + .into_data() + } + + pub async fn get_workspace_usage( + &self, + workspace_id: &str, + ) -> Result { + 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 { + 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::>() + .await? + .into_data()?; + Ok(portal_url) + } +} + +async fn get_workspace_limits( + client: &client_api::Client, + workspace_id: &str, +) -> Result { + 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::>() + .await? + .into_data() +}