use std::collections::HashMap; use chrono::Utc; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; use uuid::Uuid; use crate::dto::{CreateOrganizationReq, OrganizationResp, UpdateOrganizationReq}; use crate::entity::organization; use crate::error::{AuthError, AuthResult}; use erp_core::audit::AuditLog; use erp_core::audit_service; use erp_core::error::check_version; use erp_core::events::EventBus; /// Organization CRUD service -- create, read, update, soft-delete organizations /// within a tenant, supporting tree-structured hierarchy with path and level. pub struct OrgService; impl OrgService { /// Fetch all organizations for a tenant as a flat list (not deleted). pub async fn list_flat( tenant_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> AuthResult> { let items = organization::Entity::find() .filter(organization::Column::TenantId.eq(tenant_id)) .filter(organization::Column::DeletedAt.is_null()) .all(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; Ok(items) } /// Fetch all organizations for a tenant as a nested tree. /// /// Root nodes have `parent_id = None`. Children are grouped by `parent_id`. pub async fn get_tree( tenant_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> AuthResult> { let items = Self::list_flat(tenant_id, db).await?; Ok(build_org_tree(&items)) } /// Create a new organization within the current tenant. /// /// If `parent_id` is provided, computes `path` from the parent's path and id, /// and sets `level = parent.level + 1`. Otherwise, level defaults to 1. pub async fn create( tenant_id: Uuid, operator_id: Uuid, req: &CreateOrganizationReq, db: &sea_orm::DatabaseConnection, event_bus: &EventBus, ) -> AuthResult { // Check code uniqueness within tenant if code is provided if let Some(ref code) = req.code { let existing = organization::Entity::find() .filter(organization::Column::TenantId.eq(tenant_id)) .filter(organization::Column::Code.eq(code.as_str())) .filter(organization::Column::DeletedAt.is_null()) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; if existing.is_some() { return Err(AuthError::Validation("组织编码已存在".to_string())); } } let (path, level) = if let Some(parent_id) = req.parent_id { let parent = organization::Entity::find_by_id(parent_id) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))? .filter(|p| p.tenant_id == tenant_id && p.deleted_at.is_none()) .ok_or_else(|| AuthError::Validation("父级组织不存在".to_string()))?; let parent_path = parent.path.clone().unwrap_or_default(); let computed_path = format!("{}{}/", parent_path, parent.id); (Some(computed_path), parent.level + 1) } else { (None, 1) }; let now = Utc::now(); let id = Uuid::now_v7(); let model = organization::ActiveModel { id: Set(id), tenant_id: Set(tenant_id), name: Set(req.name.clone()), code: Set(req.code.clone()), parent_id: Set(req.parent_id), path: Set(path), level: Set(level), sort_order: Set(req.sort_order.unwrap_or(0)), created_at: Set(now), updated_at: Set(now), created_by: Set(operator_id), updated_by: Set(operator_id), deleted_at: Set(None), version: Set(1), }; model .insert(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; event_bus.publish(erp_core::events::DomainEvent::new( "organization.created", tenant_id, serde_json::json!({ "org_id": id, "name": req.name }), ), db).await; audit_service::record( AuditLog::new(tenant_id, Some(operator_id), "organization.create", "organization") .with_resource_id(id), db, ) .await; Ok(OrganizationResp { id, name: req.name.clone(), code: req.code.clone(), parent_id: req.parent_id, path: None, level, sort_order: req.sort_order.unwrap_or(0), children: vec![], version: 1, }) } /// Update editable organization fields (name, code, sort_order). pub async fn update( id: Uuid, tenant_id: Uuid, operator_id: Uuid, req: &UpdateOrganizationReq, db: &sea_orm::DatabaseConnection, ) -> AuthResult { let model = organization::Entity::find_by_id(id) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))? .filter(|o| o.tenant_id == tenant_id && o.deleted_at.is_none()) .ok_or_else(|| AuthError::Validation("组织不存在".to_string()))?; // If code is being changed, check uniqueness if let Some(ref new_code) = req.code && Some(new_code) != model.code.as_ref() { let existing = organization::Entity::find() .filter(organization::Column::TenantId.eq(tenant_id)) .filter(organization::Column::Code.eq(new_code.as_str())) .filter(organization::Column::DeletedAt.is_null()) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; if existing.is_some() { return Err(AuthError::Validation("组织编码已存在".to_string())); } } let next_ver = check_version(req.version, model.version)?; let mut active: organization::ActiveModel = model.into(); if let Some(ref name) = req.name { active.name = Set(name.clone()); } if let Some(ref code) = req.code { active.code = Set(Some(code.clone())); } if let Some(sort_order) = req.sort_order { active.sort_order = Set(sort_order); } active.updated_at = Set(Utc::now()); active.updated_by = Set(operator_id); active.version = Set(next_ver); let updated = active .update(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; audit_service::record( AuditLog::new(tenant_id, Some(operator_id), "organization.update", "organization") .with_resource_id(id), db, ) .await; Ok(OrganizationResp { id: updated.id, name: updated.name.clone(), code: updated.code.clone(), parent_id: updated.parent_id, path: updated.path.clone(), level: updated.level, sort_order: updated.sort_order, children: vec![], version: updated.version, }) } /// Soft-delete an organization by setting the `deleted_at` timestamp. pub async fn delete( id: Uuid, tenant_id: Uuid, operator_id: Uuid, db: &sea_orm::DatabaseConnection, event_bus: &EventBus, ) -> AuthResult<()> { let model = organization::Entity::find_by_id(id) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))? .filter(|o| o.tenant_id == tenant_id && o.deleted_at.is_none()) .ok_or_else(|| AuthError::Validation("组织不存在".to_string()))?; // Check for child organizations let children = organization::Entity::find() .filter(organization::Column::TenantId.eq(tenant_id)) .filter(organization::Column::ParentId.eq(id)) .filter(organization::Column::DeletedAt.is_null()) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; if children.is_some() { return Err(AuthError::Validation( "该组织下存在子组织,无法删除".to_string(), )); } let current_version = model.version; let mut active: organization::ActiveModel = model.into(); active.deleted_at = Set(Some(Utc::now())); active.updated_at = Set(Utc::now()); active.updated_by = Set(operator_id); active.version = Set(current_version + 1); active .update(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; event_bus.publish(erp_core::events::DomainEvent::new( "organization.deleted", tenant_id, serde_json::json!({ "org_id": id }), ), db).await; audit_service::record( AuditLog::new(tenant_id, Some(operator_id), "organization.delete", "organization") .with_resource_id(id), db, ) .await; Ok(()) } } /// Build a nested tree of `OrganizationResp` from a flat list of models. /// /// Root nodes (parent_id = None) form the top level. Each node recursively /// includes its children grouped by parent_id. fn build_org_tree(items: &[organization::Model]) -> Vec { let mut children_map: HashMap, Vec<&organization::Model>> = HashMap::new(); for item in items { children_map.entry(item.parent_id).or_default().push(item); } fn build_node( item: &organization::Model, map: &HashMap, Vec<&organization::Model>>, ) -> OrganizationResp { let children = map .get(&Some(item.id)) .map(|items| items.iter().map(|i| build_node(i, map)).collect()) .unwrap_or_default(); OrganizationResp { id: item.id, name: item.name.clone(), code: item.code.clone(), parent_id: item.parent_id, path: item.path.clone(), level: item.level, sort_order: item.sort_order, children, version: item.version, } } children_map .get(&None) .map(|root_items| { root_items .iter() .map(|item| build_node(item, &children_map)) .collect() }) .unwrap_or_default() }