use crate::AppService; use crate::error::AppError; use chrono::{DateTime, Utc}; use models::projects::{MemberRole, project, project_audit_log, project_members}; 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 display_name: Option, pub description: Option, pub is_public: bool, } #[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 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?; 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.display_name.unwrap_or(params.name.clone())), avatar_url: Set(None), description: Set(params.description), is_public: Set(params.is_public), created_by: Set(user.uid), 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 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?; observability::incr!(observability::PROJECTS_CREATED_TOTAL); // Initialize project billing ($20 for first project, $0 otherwise) if let Err(e) = agent::billing::initialize_project_billing(&self.db, _project.id, user.uid).await { tracing::warn!(project_id = %_project.id, error = %e, "Failed to initialize project billing — non-critical, continuing"); } 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, created_by: _project.created_by, created_at: _project.created_at, updated_at: _project.updated_at, }, }) } }