gitdataai/libs/service/workspace/init.rs
ZhenYi 07e74c230c
Some checks are pending
CI / Rust Lint & Check (push) Waiting to run
CI / Rust Tests (push) Waiting to run
CI / Frontend Lint & Type Check (push) Waiting to run
CI / Frontend Build (push) Blocked by required conditions
feat: thinking_content column + first-project budget logic
- Add thinking_content column to room_message table
- Migration for thinking_content column
- ws-protocol update with streaming chunk types
- Billing: first project gets $10, first workspace gets $30
- Subsequent projects/workspaces get $0 budget
2026-04-26 13:11:06 +08:00

122 lines
4.0 KiB
Rust

use crate::AppService;
use crate::error::AppError;
use chrono::Utc;
use models::Decimal;
use models::WorkspaceRole;
use models::workspaces::workspace;
use models::workspaces::workspace_billing;
use models::workspaces::workspace_membership;
use sea_orm::*;
use serde::{Deserialize, Serialize};
use session::Session;
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
pub struct WorkspaceInitParams {
pub slug: String,
pub name: String,
pub description: Option<String>,
}
impl AppService {
pub async fn workspace_init(
&self,
ctx: &Session,
params: WorkspaceInitParams,
) -> Result<workspace::Model, AppError> {
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
let user = self.utils_find_user_by_uid(user_uid).await?;
// Validate slug format: alphanumeric, dashes, underscores
if !params
.slug
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
return Err(AppError::BadRequest(
"Slug must contain only letters, numbers, hyphens and underscores".to_string(),
));
}
// Check slug uniqueness
if workspace::Entity::find()
.filter(workspace::Column::Slug.eq(&params.slug))
.filter(workspace::Column::DeletedAt.is_null())
.one(&self.db)
.await?
.is_some()
{
return Err(AppError::WorkspaceSlugAlreadyExists);
}
// Check name uniqueness
if workspace::Entity::find()
.filter(workspace::Column::Name.eq(&params.name))
.filter(workspace::Column::DeletedAt.is_null())
.one(&self.db)
.await?
.is_some()
{
return Err(AppError::WorkspaceNameAlreadyExists);
}
let txn = self.db.begin().await?;
let ws = workspace::ActiveModel {
id: Set(Uuid::now_v7()),
slug: Set(params.slug),
name: Set(params.name),
description: Set(params.description),
avatar_url: Set(None),
plan: Set("free".to_string()),
billing_email: Set(None),
stripe_customer_id: Set(None),
stripe_subscription_id: Set(None),
plan_expires_at: Set(None),
deleted_at: Set(None),
created_at: Set(Utc::now()),
updated_at: Set(Utc::now()),
};
let ws = ws.insert(&txn).await?;
let membership = workspace_membership::ActiveModel {
id: Default::default(),
workspace_id: Set(ws.id),
user_id: Set(user.uid),
role: Set(WorkspaceRole::Owner.to_string()),
status: Set("active".to_string()),
invited_by: Set(None),
joined_at: Set(Utc::now()),
invite_token: Set(None),
invite_expires_at: Set(None),
};
membership.insert(&txn).await?;
// Create billing record — only first workspace gets $30 initial balance
let existing_workspaces = workspace_membership::Entity::find()
.filter(workspace_membership::Column::UserId.eq(user.uid))
.filter(workspace_membership::Column::Status.eq("active"))
.all(&self.db)
.await?;
let initial_balance = if existing_workspaces.len() <= 1 {
Decimal::from_f64_retain(30.0).unwrap_or(Decimal::ZERO)
} else {
Decimal::ZERO
};
let billing = workspace_billing::ActiveModel {
workspace_id: Set(ws.id),
balance: Set(initial_balance),
currency: Set("USD".to_string()),
monthly_quota: Set(Decimal::from_f64_retain(100.0).unwrap_or(Decimal::ZERO)),
total_spent: Set(Decimal::ZERO),
updated_at: Set(Utc::now()),
created_at: Set(Utc::now()),
};
billing.insert(&txn).await?;
txn.commit().await?;
Ok(ws)
}
}
use uuid::Uuid;