use crate::AppService; use crate::error::AppError; use chrono::{DateTime, Utc}; use models::Decimal; use models::projects::{MemberRole, project, project_audit_log, project_billing, project_members}; use models::workspaces::workspace_membership; use sea_orm::*; use serde::{Deserialize, Serialize}; use session::Session; use uuid::Uuid; #[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)] pub struct ProjectInitParams { pub name: String, pub description: Option, pub is_public: bool, /// Optional workspace slug to associate this project with. pub workspace_slug: Option, } #[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)] pub struct ProjectInitResponse { pub params: ProjectInitParams, pub project: ProjectModel, } #[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)] pub struct ProjectModel { pub uid: Uuid, pub name: String, pub display_name: String, pub avatar_url: Option, pub description: Option, pub is_public: bool, pub workspace_id: Option, pub created_by: Uuid, pub created_at: DateTime, pub updated_at: DateTime, } impl AppService { pub async fn project_init( &self, ctx: &Session, params: ProjectInitParams, ) -> Result { let inner = params.clone(); if let Ok(_) = self.utils_find_project_by_name(params.name.clone()).await { return Err(AppError::ProjectNameAlreadyExists); } let user = ctx.user().ok_or(AppError::Unauthorized)?; let user = self.utils_find_user_by_uid(user).await?; // Resolve workspace if provided let workspace_id = match ¶ms.workspace_slug { Some(slug) => { let ws = self.utils_find_workspace_by_slug(slug.clone()).await?; let membership = workspace_membership::Entity::find() .filter(workspace_membership::Column::WorkspaceId.eq(ws.id)) .filter(workspace_membership::Column::UserId.eq(user.uid)) .filter(workspace_membership::Column::Status.eq("active")) .one(&self.db) .await?; if membership.is_none() { return Err(AppError::NotWorkspaceMember); } Some(ws.id) } None => None, }; let project_uid = Uuid::now_v7(); let txn = self.db.begin().await?; let project = project::ActiveModel { id: Set(project_uid), name: Set(params.name.clone()), display_name: Set(params.name), avatar_url: Set(None), description: Set(params.description), is_public: Set(params.is_public), created_by: Set(user.uid), workspace_id: Set(workspace_id), created_at: Set(Utc::now()), updated_at: Set(Utc::now()), }; let _project = project.insert(&txn).await?; let project_member = project_members::ActiveModel { id: Default::default(), project: Set(_project.id), user: Set(user.uid), scope: Set(MemberRole::Owner.to_string()), joined_at: Set(Utc::now()), }; project_member.insert(&txn).await?; let billing = project_billing::ActiveModel { project: Set(_project.id), balance: Set(Decimal::from(200i64)), currency: Set("USD".to_string()), user: Set(Some(user.uid)), updated_at: Set(Utc::now()), created_at: Set(Utc::now()), ..Default::default() }; billing.insert(&txn).await?; let log = project_audit_log::ActiveModel { project: Set(_project.id), actor: Set(user.uid), action: Set("project_create".to_string()), details: Set(Some(serde_json::json!({ "project_name": _project.name.clone(), "project_uid": _project.id, "is_public": _project.is_public, "description": _project.description.clone(), }))), created_at: Set(Utc::now()), ..Default::default() }; log.insert(&txn).await?; txn.commit().await?; Ok(ProjectInitResponse { params: inner, project: ProjectModel { uid: _project.id, name: _project.name.clone(), display_name: _project.display_name.clone(), avatar_url: _project.avatar_url.clone(), description: _project.description.clone(), is_public: _project.is_public, workspace_id: _project.workspace_id, created_by: _project.created_by, created_at: _project.created_at, updated_at: _project.updated_at, }, }) } }