Jacob Schmidt 6dda184d54 Wire store checkout flow across client and server
- Add checkout request/response bridge and workspace re-hydration in store UI
- Implement server-side checkout stores for charging bank/cash and granting locker/VA items
- Normalize catalog/cart payload categories and fix locker VA sync event naming
2026-03-12 21:44:19 -05:00

241 lines
9.3 KiB
Rust

//! 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<R: OrgRepository> {
/// 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<R: OrgRepository> OrgService<R> {
fn normalize_org_value(
mut org_value: serde_json::Value,
key_override: Option<String>,
) -> Result<Org, String> {
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>(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<Org, String> {
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<Org, String> {
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<Org, String> {
// 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<String, CreditLineSummary>,
>(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<bool, String> {
// 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<Vec<MemberSummary>, 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)
}
}