lmrc-cli 0.3.16

CLI tool for scaffolding LMRC Stack infrastructure projects
Documentation
//! User service - Business logic (application core)
//!
//! This is where your business logic lives. The service layer:
//! - Validates business rules
//! - Orchestrates operations
//! - Handles transactions
//! - Remains independent of HTTP/database specifics
//!
//! In hexagonal architecture, this is the "core" - it defines what
//! the application does, without knowing how (adapters handle "how").

use crate::error::{AppError, Result};
use crate::features::examples::models::{
    CreateUserRequest, UpdateUserRequest, User, UserListResponse,
};
use crate::features::examples::repository::UserRepository;
use uuid::Uuid;

/// Service for user business logic
#[derive(Clone)]
pub struct UserService {
    repository: UserRepository,
}

impl UserService {
    /// Create a new user service
    pub fn new(repository: UserRepository) -> Self {
        Self { repository }
    }

    /// Get a user by ID
    pub async fn get_user(&self, id: Uuid) -> Result<User> {
        self.repository
            .find_by_id(id)
            .await?
            .ok_or_else(|| AppError::NotFound(format!("User with id {} not found", id)))
    }

    /// List users with pagination
    pub async fn list_users(&self, page: u64, per_page: u64) -> Result<UserListResponse> {
        // Validate pagination parameters
        if page == 0 {
            return Err(AppError::BadRequest("Page must be at least 1".to_string()));
        }

        if per_page == 0 || per_page > 100 {
            return Err(AppError::BadRequest(
                "Per page must be between 1 and 100".to_string(),
            ));
        }

        let (users, total) = self.repository.list(page, per_page).await?;

        Ok(UserListResponse {
            users,
            total,
            page,
            per_page,
        })
    }

    /// Create a new user
    pub async fn create_user(&self, request: CreateUserRequest) -> Result<User> {
        // Validate request
        request
            .validate()
            .map_err(|e| AppError::Validation(e.to_string()))?;

        // Business rule: Email must be unique
        if self.repository.email_exists(&request.email).await? {
            return Err(AppError::BadRequest(format!(
                "User with email {} already exists",
                request.email
            )));
        }

        // Additional business rules can be added here
        // For example:
        // - Check if domain is allowed
        // - Send welcome email
        // - Create audit log entry
        // - etc.

        self.repository.create(&request).await
    }

    /// Update an existing user
    pub async fn update_user(&self, id: Uuid, request: UpdateUserRequest) -> Result<User> {
        // Validate request
        request
            .validate()
            .map_err(|e| AppError::Validation(e.to_string()))?;

        // Business rule: If changing email, it must be unique
        if let Some(new_email) = &request.email {
            if let Some(existing_user) = self.repository.find_by_email(new_email).await? {
                if existing_user.id != id {
                    return Err(AppError::BadRequest(format!(
                        "User with email {} already exists",
                        new_email
                    )));
                }
            }
        }

        // Additional business rules can be added here
        // For example:
        // - Validate email domain
        // - Send notification about profile change
        // - Create audit log entry
        // - etc.

        self.repository.update(id, &request).await
    }

    /// Delete a user
    pub async fn delete_user(&self, id: Uuid) -> Result<()> {
        // Business rule: Check if user exists before deletion
        self.get_user(id).await?;

        // Additional business rules can be added here
        // For example:
        // - Check if user has pending orders
        // - Send account deletion confirmation
        // - Archive user data
        // - Create audit log entry
        // - etc.

        self.repository.delete(id).await
    }

    /// Search users by email (exact match)
    pub async fn search_by_email(&self, email: &str) -> Result<Option<User>> {
        self.repository.find_by_email(email).await
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // Note: Integration tests with real database are in tests/integration/
    // These are just compilation tests

    #[test]
    fn test_service_compiles() {
        // Ensure the service API is sound
    }
}