//! Organization service layer providing business logic for organization management operations. //! //! Implements the service layer of the organization management system, handling business logic, //! validation, and orchestration. //! //! For full documentation, architecture, and examples, see the [crate README](../README.md). use forge_models::{CreditLineSummary, MemberSummary, Org}; use forge_repositories::OrgRepository; use std::collections::HashMap; /// Service layer implementation for organization business logic and operations. /// /// Orchestrates organization management operations, handling business logic, validation, /// and data transformation. See [crate README](../README.md) for details. /// /// # Thread Safety /// Thread-safe when used with a thread-safe repository. pub struct OrgService { /// The repository instance used for all data persistence operations. /// /// This repository handles the actual storage and retrieval of organization /// and member data, abstracting away the specific database implementation details. repository: R, } impl OrgService { fn normalize_org_value( mut org_value: serde_json::Value, key_override: Option, ) -> Result { let org_object = org_value .as_object_mut() .ok_or_else(|| "Org payload must be a JSON object".to_string())?; if let Some(key) = key_override { org_object.insert("id".to_string(), serde_json::Value::String(key)); } if matches!( org_object.get("credit_lines"), Some(serde_json::Value::Array(lines)) if lines.is_empty() ) { org_object.insert( "credit_lines".to_string(), serde_json::Value::Object(serde_json::Map::new()), ); } serde_json::from_value::(org_value).map_err(|e| format!("Invalid Org JSON: {}", e)) } /// Creates a new organization service with the provided repository. /// /// The repository must be initialized and ready for use. pub fn new(repository: R) -> Self { Self { repository } } /// Creates a new organization with the provided ID and JSON data. /// /// Handles validation, duplicate checking, and persistence. /// See [crate README](../README.md) for JSON format and business rules. pub fn create_org(&self, key: String, json_data: String) -> Result { let org_value: serde_json::Value = serde_json::from_str(&json_data).map_err(|e| format!("Invalid Org JSON: {}", e))?; let org = Self::normalize_org_value(org_value, Some(key))?; // Validate organization name is not empty if org.name.trim().is_empty() { return Err("Organization name cannot be empty".to_string()); } // Check if organization already exists to prevent duplicates if self.repository.exists(&org.id)? { return Err(format!("Organization with ID '{}' already exists", org.id)); } // Store the organization in the repository self.repository.create(&org)?; Ok(org) } pub fn get_org(&self, key: String) -> Result { self.repository .get_by_id(&key)? .ok_or_else(|| format!("Organization with ID '{}' not found", key)) } /// Updates an existing organization with new data from JSON. /// /// Handles partial updates, validation, and persistence. /// See [crate README](../README.md) for JSON format and concurrency details. pub fn update_org(&self, key: String, json_update: String) -> Result { // Retrieve existing organization from repository let mut org = match self.repository.get_by_id(&key)? { Some(org) => org, None => return Err(format!("Organization with ID '{}' not found", key)), }; // Parse and validate JSON update data let mut update_data: serde_json::Value = serde_json::from_str(&json_update).map_err(|e| format!("Invalid JSON: {}", e))?; // Ensure update data is a JSON object if !update_data.is_object() { return Err("Update data must be a JSON object".to_string()); } if matches!( update_data.get("credit_lines"), Some(serde_json::Value::Array(lines)) if lines.is_empty() ) { update_data["credit_lines"] = serde_json::Value::Object(serde_json::Map::new()); } // Create a temporary copy to safely apply updates with validation let mut updated_org = org.clone(); // Apply updates field by field if let Some(obj) = update_data.as_object() { for (field, value) in obj { match field.as_str() { "id" => { if let Some(id_str) = value.as_str() { updated_org.id = id_str.to_string(); } else { return Err("ID must be a string".to_string()); } } "owner" => { if let Some(owner_str) = value.as_str() { updated_org.owner = owner_str.to_string(); } else { return Err("Owner must be a string".to_string()); } } "name" => { if let Some(name_str) = value.as_str() { updated_org.name = name_str.to_string(); } else { return Err("Name must be a string".to_string()); } } "funds" => { if let Some(funds_val) = value.as_f64() { updated_org.funds = funds_val; } else { return Err("Funds must be a number".to_string()); } } "reputation" => { if let Some(rep_val) = value.as_i64() { updated_org.reputation = rep_val; } else { return Err("Reputation must be an integer".to_string()); } } "credit_lines" => { if value.is_null() { updated_org.credit_lines = HashMap::new(); } else { updated_org.credit_lines = serde_json::from_value::< HashMap, >(value.clone()) .map_err(|e| { format!( "Credit lines must be an object of member credit entries: {}", e ) })?; } } _ => { return Err(format!("Unknown field: {}", field)); } } } } // Validate the updated organization before committing changes updated_org .validate() .map_err(|e| format!("Validation failed: {}", e))?; // Only commit changes after validation passes org = updated_org; // Persist the updated organization to repository self.repository.update(&org)?; Ok(org) } /// Permanently deletes an organization from the system. /// /// Irreversible operation. Delegates to repository. pub fn delete_org(&self, key: String) -> Result<(), String> { self.repository.delete(&key) } /// Checks if an organization exists in the system. /// /// Lightweight check without data retrieval. pub fn org_exists(&self, key: String) -> Result { // Delegate existence check to repository layer self.repository.exists(&key) } /// Adds a new member UID to an organization with validation. pub fn add_member(&self, key: String, member_uid: String) -> Result<(), String> { // Verify organization exists before adding member if !self.repository.exists(&key)? { return Err(format!("Organization with ID '{}' not found", key)); } // Add member UID to organization through repository self.repository.add_member(&key, &member_uid) } /// Retrieves all members of an organization as a UID to name mapping. pub fn get_members(&self, key: String) -> Result, String> { // Delegate member retrieval to repository layer self.repository.get_members(&key) } /// Permanently removes a specific member from an organization. /// /// Irreversible operation. Delegates to repository. pub fn remove_member(&self, key: String, member_uid: String) -> Result<(), String> { // Verify organization exists before attempting member removal if !self.repository.exists(&key)? { return Err(format!("Organization with ID '{}' not found", key)); } // Delegate member removal to repository layer self.repository.remove_member(&key, &member_uid) } }