> For the complete documentation index, see [llms.txt](https://docs.beeoclock.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.beeoclock.com/services/expense-management/usecase.md).

# Use Case Documentation

## Overview

This document outlines the use cases for the Expense Management module in the Bee O'clock panel service. Each use case represents a specific business operation that can be performed within the expense management system, following the CQRS pattern and Clean Architecture principles.

## Architecture Overview

The expense management use cases are organized following the Clean Architecture pattern:

* **Application Layer**: Use cases that orchestrate business operations
* **Domain Layer**: Business rules and domain entities
* **Infrastructure Layer**: Data persistence and external service integration

Each use case implements a specific interface and encapsulates the business logic for a particular operation while maintaining separation of concerns.

## Command Use Cases

### 1. Create Expense Use Case

**File:** `applications/panel.beeoclock/src/modules/expense/application/use-cases/create-expense.usecase.ts`

**Purpose:** Creates a new expense record with validation, business rule enforcement, and data persistence.

```typescript
import { Inject, Injectable } from '@nestjs/common';
import { ICreateExpenseUseCase } from '../interfaces/i.create-expense.use-case';
import { ExpenseDto } from '../dtos/expense.dto';
import { IExpenseRepository } from '../../domain/interfaces/i.expense.repository';
import { EXPENSE_REPOSITORY_TOKEN } from '../tokens/expense.repository.token';
import { ExpenseMapperToDomain } from '../mappers/expense.mapper-to-domain';
import { ExpenseMapperToDto } from '../mappers/expense.mapper-to-dto';

@Injectable()
export class CreateExpenseUseCase implements ICreateExpenseUseCase {
  constructor(
    @Inject(EXPENSE_REPOSITORY_TOKEN)
    private readonly expenseRepository: IExpenseRepository
  ) {}

  /**
   * Execute expense creation with comprehensive validation and business rule enforcement
   * @param expenseDto - Expense data to create
   * @returns Promise resolving to created expense DTO
   */
  async execute(expenseDto: ExpenseDto): Promise<ExpenseDto> {
    // Convert DTO to domain entity
    const expenseDomain = ExpenseMapperToDomain.map(expenseDto);

    // Validate business rules
    this.validateExpenseBusinessRules(expenseDomain);

    // Calculate and validate total amount
    this.validateAndCalculateTotalAmount(expenseDomain);

    // Validate expense items
    this.validateExpenseItems(expenseDomain);

    // Persist the expense
    const createdExpense = await this.expenseRepository.create(expenseDomain);

    // Convert back to DTO and return
    return ExpenseMapperToDto.map(createdExpense);
  }

  /**
   * Validate core business rules for expense creation
   * @param expense - Domain expense entity
   */
  private validateExpenseBusinessRules(expense: IExpense): void {
    // Validate expense date is not in the future beyond reasonable limits
    const maxFutureDate = new Date();
    maxFutureDate.setDate(maxFutureDate.getDate() + 30); // Allow 30 days in future
    
    if (expense.expensedAt > maxFutureDate) {
      throw new BadRequestException('Expense date cannot be more than 30 days in the future');
    }

    // Validate minimum expense amount
    if (expense.totalValue.amount <= 0) {
      throw new BadRequestException('Expense amount must be greater than zero');
    }

    // Validate maximum expense amount for fraud prevention
    const maxAllowedAmount = 100000; // $100,000 limit
    if (expense.totalValue.amount > maxAllowedAmount) {
      throw new BadRequestException(`Expense amount exceeds maximum allowed limit of ${maxAllowedAmount}`);
    }
  }

  /**
   * Validate and calculate total amount from expense items
   * @param expense - Domain expense entity
   */
  private validateAndCalculateTotalAmount(expense: IExpense): void {
    if (!expense.items || expense.items.length === 0) {
      return; // Simple expense without line items
    }

    // Calculate total from line items
    const calculatedTotal = expense.items.reduce(
      (sum, item) => sum + item.itemValue.amount,
      0
    );

    // Validate currency consistency across all items
    const baseCurrency = expense.totalValue.currency;
    const invalidCurrencyItems = expense.items.filter(
      item => item.itemValue.currency !== baseCurrency
    );

    if (invalidCurrencyItems.length > 0) {
      throw new BadRequestException(
        `All expense items must use the same currency: ${baseCurrency}`
      );
    }

    // Validate total amount matches sum of line items (with small tolerance for rounding)
    const tolerance = 0.01; // 1 cent tolerance
    const difference = Math.abs(calculatedTotal - expense.totalValue.amount);
    
    if (difference > tolerance) {
      throw new BadRequestException(
        `Total expense amount (${expense.totalValue.amount}) does not match sum of line items (${calculatedTotal})`
      );
    }
  }

  /**
   * Validate individual expense items
   * @param expense - Domain expense entity
   */
  private validateExpenseItems(expense: IExpense): void {
    if (!expense.items) {
      return;
    }

    expense.items.forEach((item, index) => {
      // Validate item amount
      if (item.itemValue.amount <= 0) {
        throw new BadRequestException(`Expense item ${index + 1} amount must be greater than zero`);
      }

      // Validate item description
      if (!item.description || item.description.trim().length === 0) {
        throw new BadRequestException(`Expense item ${index + 1} must have a description`);
      }

      // Validate categories
      if (!item.categories || item.categories.length === 0) {
        throw new BadRequestException(`Expense item ${index + 1} must have at least one category`);
      }

      // Validate source information
      if (!item.source || !item.source.sourceId) {
        throw new BadRequestException(`Expense item ${index + 1} must have a valid source`);
      }
    });
  }
}
```

**Business Rules:**

* Expense amounts must be positive and within reasonable limits
* Future-dated expenses are limited to 30 days ahead
* Currency consistency across all line items is enforced
* Total amount must match the sum of line items (within rounding tolerance)
* Each line item must have description, categories, and valid source
* Maximum expense amount is enforced for fraud prevention

**Error Scenarios:**

* Invalid expense amount (negative, zero, or exceeding limits)
* Future date beyond allowed range
* Currency mismatch between total and line items
* Missing required fields (description, categories, source)
* Total amount mismatch with line item sum

## Query Use Cases

### 1. Get Paged Expenses Use Case

**File:** `applications/panel.beeoclock/src/modules/expense/application/use-cases/get-paged-expenses.usecase.ts`

**Purpose:** Retrieves a paginated list of expenses with advanced filtering capabilities.

```typescript
import { Inject, Injectable } from '@nestjs/common';
import { IGetPagedExpensesUseCase } from '../interfaces/i.get-paged-expenses.use-case';
import { ExpensePaginationDto } from '../dtos/expense.pagination.dto';
import { PaginationResponseDto } from '@beeoclock/common/application/dto/pagination.response.dto';
import { ExpenseDto } from '../dtos/expense.dto';
import { IExpenseRepository } from '../../domain/interfaces/i.expense.repository';
import { EXPENSE_REPOSITORY_TOKEN } from '../tokens/expense.repository.token';
import { ExpenseMapperToDto } from '../mappers/expense.mapper-to-dto';

@Injectable()
export class GetPagedExpensesUseCase implements IGetPagedExpensesUseCase {
  constructor(
    @Inject(EXPENSE_REPOSITORY_TOKEN)
    private readonly expenseRepository: IExpenseRepository
  ) {}

  /**
   * Execute paginated expense retrieval with advanced filtering
   * @param pagination - Pagination and filter parameters
   * @returns Promise resolving to paginated expense results
   */
  async execute(
    pagination: ExpensePaginationDto
  ): Promise<PaginationResponseDto<ExpenseDto>> {
    // Validate pagination parameters
    this.validatePaginationParameters(pagination);

    // Normalize and prepare filter parameters
    const normalizedPagination = this.normalizePaginationParameters(pagination);

    // Retrieve paginated expenses from repository
    const paginatedExpenses = await this.expenseRepository.findManyByPagination(
      normalizedPagination
    );

    // Convert domain entities to DTOs
    const expenseDtos = ExpenseMapperToDto.mapArray(paginatedExpenses.items);

    // Return paginated response
    return {
      items: expenseDtos,
      totalSize: paginatedExpenses.totalSize,
      page: paginatedExpenses.page,
      size: paginatedExpenses.size
    };
  }

  /**
   * Validate pagination parameters for security and performance
   * @param pagination - Pagination parameters to validate
   */
  private validatePaginationParameters(pagination: ExpensePaginationDto): void {
    // Validate page number
    if (pagination.page && pagination.page < 1) {
      throw new BadRequestException('Page number must be 1 or greater');
    }

    // Validate page size limits
    const maxPageSize = 100;
    if (pagination.size && pagination.size > maxPageSize) {
      throw new BadRequestException(`Page size cannot exceed ${maxPageSize} items`);
    }

    if (pagination.size && pagination.size < 1) {
      throw new BadRequestException('Page size must be 1 or greater');
    }

    // Validate date range parameters
    if (pagination.start && pagination.end) {
      const startDate = new Date(pagination.start);
      const endDate = new Date(pagination.end);

      if (startDate > endDate) {
        throw new BadRequestException('Start date must be before or equal to end date');
      }

      // Validate date range is not too large (prevent performance issues)
      const maxRangeDays = 365; // 1 year maximum
      const daysDifference = (endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24);
      
      if (daysDifference > maxRangeDays) {
        throw new BadRequestException(`Date range cannot exceed ${maxRangeDays} days`);
      }
    }

    // Validate search phrase length
    if (pagination.phrase && pagination.phrase.length > 100) {
      throw new BadRequestException('Search phrase cannot exceed 100 characters');
    }

    // Validate expense categories array
    if (pagination.expenseCategories) {
      const maxCategories = 20;
      if (pagination.expenseCategories.length > maxCategories) {
        throw new BadRequestException(`Cannot filter by more than ${maxCategories} categories`);
      }

      // Validate individual category names
      pagination.expenseCategories.forEach(category => {
        if (!category || category.trim().length === 0) {
          throw new BadRequestException('Expense category names cannot be empty');
        }
        if (category.length > 50) {
          throw new BadRequestException('Expense category names cannot exceed 50 characters');
        }
      });
    }
  }

  /**
   * Normalize pagination parameters with defaults and sanitization
   * @param pagination - Raw pagination parameters
   * @returns Normalized pagination parameters
   */
  private normalizePaginationParameters(
    pagination: ExpensePaginationDto
  ): ExpensePaginationDto {
    return {
      ...pagination,
      page: pagination.page || 1,
      size: Math.min(pagination.size || 10, 100), // Default 10, max 100
      phrase: pagination.phrase?.trim(),
      expenseCategories: pagination.expenseCategories?.map(cat => cat.trim()).filter(Boolean)
    };
  }
}
```

**Business Rules:**

* Page size is limited to 100 items for performance
* Date ranges are limited to 1 year to prevent performance issues
* Search phrases are limited to 100 characters
* Maximum of 20 expense categories can be filtered at once
* Start date must be before or equal to end date
* Empty or invalid category names are filtered out

**Query Capabilities:**

* Pagination with configurable page size
* Text search across expense descriptions
* Date range filtering (start and end dates)
* Multiple expense category filtering
* Sorting by expense date and creation time

### 2. Get Expense by ID Use Case

**Purpose:** Retrieves a specific expense by its unique identifier with permission validation.

```typescript
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
import { IGetExpenseByIdUseCase } from '../interfaces/i.get-expense-by-id.use-case';
import { ExpenseDto } from '../dtos/expense.dto';
import { IExpenseRepository } from '../../domain/interfaces/i.expense.repository';
import { EXPENSE_REPOSITORY_TOKEN } from '../tokens/expense.repository.token';
import { ExpenseMapperToDto } from '../mappers/expense.mapper-to-dto';

@Injectable()
export class GetExpenseByIdUseCase implements IGetExpenseByIdUseCase {
  constructor(
    @Inject(EXPENSE_REPOSITORY_TOKEN)
    private readonly expenseRepository: IExpenseRepository
  ) {}

  /**
   * Execute expense retrieval by ID with validation
   * @param id - Expense identifier
   * @returns Promise resolving to expense DTO
   */
  async execute(id: string): Promise<ExpenseDto> {
    // Validate expense ID format
    this.validateExpenseId(id);

    // Retrieve expense from repository
    const expense = await this.expenseRepository.findOneById(id);

    // Handle not found case
    if (!expense) {
      throw new NotFoundException(`Expense with ID ${id} not found`);
    }

    // Convert to DTO and return
    return ExpenseMapperToDto.map(expense);
  }

  /**
   * Validate expense ID format and constraints
   * @param id - Expense identifier to validate
   */
  private validateExpenseId(id: string): void {
    if (!id || id.trim().length === 0) {
      throw new BadRequestException('Expense ID cannot be empty');
    }

    // Validate MongoDB ObjectId format (24 hex characters)
    const objectIdRegex = /^[0-9a-fA-F]{24}$/;
    if (!objectIdRegex.test(id)) {
      throw new BadRequestException('Invalid expense ID format');
    }
  }
}
```

**Business Rules:**

* Expense ID must be a valid MongoDB ObjectId format
* Expense must exist in the current tenant's scope
* User must have read permissions for expense data

## Category Management Use Cases

### 1. Create Expense Category Use Case

**Purpose:** Creates a new expense category with validation and uniqueness checks.

```typescript
import { Inject, Injectable, ConflictException } from '@nestjs/common';
import { ICreateExpenseCategoryUseCase } from '../interfaces/i.create-expense-category.use-case';
import { ExpenseCategoryDto } from '../dtos/expense-category.dto';
import { IExpenseCategoryRepository } from '../../domain/interfaces/i.expense-category.repository';
import { EXPENSE_CATEGORY_REPOSITORY_TOKEN } from '../tokens/expense-category.repository.token';
import { ExpenseCategoryMapperToDomain } from '../mappers/expense-category.mapper-to-domain';

@Injectable()
export class CreateExpenseCategoryUseCase implements ICreateExpenseCategoryUseCase {
  constructor(
    @Inject(EXPENSE_CATEGORY_REPOSITORY_TOKEN)
    private readonly expenseCategoryRepository: IExpenseCategoryRepository
  ) {}

  /**
   * Execute expense category creation with validation
   * @param categoryDto - Category data to create
   * @returns Promise resolving when category is created
   */
  async execute(categoryDto: ExpenseCategoryDto): Promise<void> {
    // Validate category data
    this.validateCategoryData(categoryDto);

    // Check for existing category with same name
    await this.validateCategoryUniqueness(categoryDto.name);

    // Convert DTO to domain entity
    const categoryDomain = ExpenseCategoryMapperToDomain.map(categoryDto);

    // Persist the category
    await this.expenseCategoryRepository.create(categoryDomain);
  }

  /**
   * Validate category data constraints
   * @param category - Category data to validate
   */
  private validateCategoryData(category: ExpenseCategoryDto): void {
    // Validate name constraints
    if (!category.name || category.name.trim().length === 0) {
      throw new BadRequestException('Category name cannot be empty');
    }

    if (category.name.length > 50) {
      throw new BadRequestException('Category name cannot exceed 50 characters');
    }

    // Validate name format (alphanumeric, hyphens, underscores)
    const nameRegex = /^[a-zA-Z0-9\-_\s]+$/;
    if (!nameRegex.test(category.name)) {
      throw new BadRequestException(
        'Category name can only contain letters, numbers, hyphens, underscores, and spaces'
      );
    }

    // Validate description if provided
    if (category.description && category.description.length > 500) {
      throw new BadRequestException('Category description cannot exceed 500 characters');
    }
  }

  /**
   * Validate that category name is unique within tenant
   * @param name - Category name to check
   */
  private async validateCategoryUniqueness(name: string): Promise<void> {
    const existingCategory = await this.expenseCategoryRepository.findOneByName(name);
    
    if (existingCategory) {
      throw new ConflictException(`Expense category with name '${name}' already exists`);
    }
  }
}
```

**Business Rules:**

* Category names must be unique within a tenant
* Category names are limited to 50 characters
* Category names can only contain alphanumeric characters, hyphens, underscores, and spaces
* Category descriptions are optional and limited to 500 characters
* Category names cannot be empty or whitespace-only

### 2. Create Multiple Expense Categories Use Case

**Purpose:** Creates multiple expense categories in a single operation with batch validation.

```typescript
import { Inject, Injectable } from '@nestjs/common';
import { ICreateManyExpenseCategoriesUseCase } from '../interfaces/i.create-many-expense-categories.use-case';
import { ExpenseCategoryDto } from '../dtos/expense-category.dto';
import { IExpenseCategoryRepository } from '../../domain/interfaces/i.expense-category.repository';
import { EXPENSE_CATEGORY_REPOSITORY_TOKEN } from '../tokens/expense-category.repository.token';
import { ExpenseCategoryMapperToDomain } from '../mappers/expense-category.mapper-to-domain';

@Injectable()
export class CreateManyExpenseCategoriesUseCase implements ICreateManyExpenseCategoriesUseCase {
  constructor(
    @Inject(EXPENSE_CATEGORY_REPOSITORY_TOKEN)
    private readonly expenseCategoryRepository: IExpenseCategoryRepository
  ) {}

  /**
   * Execute bulk expense category creation with validation
   * @param categoriesDto - Array of category data to create
   * @returns Promise resolving when all categories are created
   */
  async execute(categoriesDto: ExpenseCategoryDto[]): Promise<void> {
    // Validate input array
    this.validateCategoriesArray(categoriesDto);

    // Validate each category individually
    categoriesDto.forEach((category, index) => {
      try {
        this.validateCategoryData(category);
      } catch (error) {
        throw new BadRequestException(`Validation error in category ${index + 1}: ${error.message}`);
      }
    });

    // Check for internal duplicates
    this.validateNoDuplicateNames(categoriesDto);

    // Check for existing categories with same names
    await this.validateBatchUniqueness(categoriesDto);

    // Convert DTOs to domain entities
    const categoriesDomain = categoriesDto.map(dto => 
      ExpenseCategoryMapperToDomain.map(dto)
    );

    // Persist all categories in batch
    await this.expenseCategoryRepository.createMany(categoriesDomain);
  }

  /**
   * Validate the categories array constraints
   * @param categories - Array of categories to validate
   */
  private validateCategoriesArray(categories: ExpenseCategoryDto[]): void {
    if (!categories || !Array.isArray(categories)) {
      throw new BadRequestException('Categories must be provided as an array');
    }

    if (categories.length === 0) {
      throw new BadRequestException('At least one category must be provided');
    }

    const maxBatchSize = 50;
    if (categories.length > maxBatchSize) {
      throw new BadRequestException(`Cannot create more than ${maxBatchSize} categories at once`);
    }
  }

  /**
   * Validate that there are no duplicate names within the batch
   * @param categories - Array of categories to check
   */
  private validateNoDuplicateNames(categories: ExpenseCategoryDto[]): void {
    const names = categories.map(cat => cat.name.toLowerCase().trim());
    const uniqueNames = new Set(names);

    if (names.length !== uniqueNames.size) {
      const duplicates = names.filter((name, index) => names.indexOf(name) !== index);
      throw new BadRequestException(`Duplicate category names found: ${duplicates.join(', ')}`);
    }
  }

  /**
   * Validate that none of the categories already exist
   * @param categories - Array of categories to check
   */
  private async validateBatchUniqueness(categories: ExpenseCategoryDto[]): Promise<void> {
    const existingChecks = categories.map(category =>
      this.expenseCategoryRepository.findOneByName(category.name)
    );

    const existingCategories = await Promise.all(existingChecks);
    const conflictingNames = existingCategories
      .map((existing, index) => existing ? categories[index].name : null)
      .filter(Boolean);

    if (conflictingNames.length > 0) {
      throw new ConflictException(
        `The following category names already exist: ${conflictingNames.join(', ')}`
      );
    }
  }

  /**
   * Validate individual category data (reused from single creation)
   * @param category - Category data to validate
   */
  private validateCategoryData(category: ExpenseCategoryDto): void {
    // Implementation same as CreateExpenseCategoryUseCase.validateCategoryData
    // ... (validation logic)
  }
}
```

**Business Rules:**

* Maximum of 50 categories can be created in a single batch operation
* No duplicate names are allowed within the batch
* All category names must be unique across the tenant (including existing categories)
* Each category must pass individual validation rules
* Batch operations are atomic (all succeed or all fail)

### 3. Get Paged Expense Categories Use Case

**Purpose:** Retrieves a paginated list of expense categories for selection and management.

```typescript
import { Inject, Injectable } from '@nestjs/common';
import { IGetPagedExpenseCategoriesUseCase } from '../interfaces/i.get-paged-expense-categories.use-case';
import { PaginationRequestDto } from '@beeoclock/common/application/dto/pagination.request.dto';
import { PaginationResponseDto } from '@beeoclock/common/application/dto/pagination.response.dto';
import { ExpenseCategoryDto } from '../dtos/expense-category.dto';
import { IExpenseCategoryRepository } from '../../domain/interfaces/i.expense-category.repository';
import { EXPENSE_CATEGORY_REPOSITORY_TOKEN } from '../tokens/expense-category.repository.token';
import { ExpenseCategoryMapperToDto } from '../mappers/expense-category.mapper-to-dto';

@Injectable()
export class GetPagedExpenseCategoriesUseCase implements IGetPagedExpenseCategoriesUseCase {
  constructor(
    @Inject(EXPENSE_CATEGORY_REPOSITORY_TOKEN)
    private readonly expenseCategoryRepository: IExpenseCategoryRepository
  ) {}

  /**
   * Execute paginated expense category retrieval
   * @param pagination - Pagination parameters
   * @returns Promise resolving to paginated category results
   */
  async execute(
    pagination: PaginationRequestDto
  ): Promise<PaginationResponseDto<ExpenseCategoryDto>> {
    // Validate pagination parameters
    this.validatePaginationParameters(pagination);

    // Apply defaults and normalize parameters
    const normalizedPagination = this.normalizePaginationParameters(pagination);

    // Retrieve paginated categories from repository
    const paginatedCategories = await this.expenseCategoryRepository.findManyByPagination(
      normalizedPagination
    );

    // Convert domain entities to DTOs
    const categoryDtos = ExpenseCategoryMapperToDto.mapArray(paginatedCategories.items);

    // Return paginated response
    return {
      items: categoryDtos,
      totalSize: paginatedCategories.totalSize,
      page: paginatedCategories.page,
      size: paginatedCategories.size
    };
  }

  /**
   * Validate pagination parameters
   * @param pagination - Pagination parameters to validate
   */
  private validatePaginationParameters(pagination: PaginationRequestDto): void {
    if (pagination.page && pagination.page < 1) {
      throw new BadRequestException('Page number must be 1 or greater');
    }

    const maxPageSize = 100;
    if (pagination.size && pagination.size > maxPageSize) {
      throw new BadRequestException(`Page size cannot exceed ${maxPageSize} items`);
    }

    if (pagination.size && pagination.size < 1) {
      throw new BadRequestException('Page size must be 1 or greater');
    }
  }

  /**
   * Normalize pagination parameters with defaults
   * @param pagination - Raw pagination parameters
   * @returns Normalized pagination parameters
   */
  private normalizePaginationParameters(
    pagination: PaginationRequestDto
  ): PaginationRequestDto {
    return {
      ...pagination,
      page: pagination.page || 1,
      size: Math.min(pagination.size || 20, 100) // Default 20 for categories, max 100
    };
  }
}
```

**Business Rules:**

* Default page size is 20 categories (higher than expenses since categories are typically fewer)
* Maximum page size is 100 categories
* Categories are sorted alphabetically by name for consistency
* Only active (non-deleted) categories are returned

## Use Case Dependencies and Integration

### Dependency Injection Tokens

```typescript
// Use case tokens for dependency injection
export const CREATE_EXPENSE_USE_CASE_TOKEN = Symbol('ICreateExpenseUseCase');
export const GET_PAGED_EXPENSES_USE_CASE_TOKEN = Symbol('IGetPagedExpensesUseCase');
export const GET_EXPENSE_BY_ID_USE_CASE_TOKEN = Symbol('IGetExpenseByIdUseCase');
export const CREATE_EXPENSE_CATEGORY_USE_CASE_TOKEN = Symbol('ICreateExpenseCategoryUseCase');
export const CREATE_MANY_EXPENSE_CATEGORIES_USE_CASE_TOKEN = Symbol('ICreateManyExpenseCategoriesUseCase');
export const GET_PAGED_EXPENSE_CATEGORIES_USE_CASE_TOKEN = Symbol('IGetPagedExpenseCategoriesUseCase');

// Repository tokens
export const EXPENSE_REPOSITORY_TOKEN = Symbol('IExpenseRepository');
export const EXPENSE_CATEGORY_REPOSITORY_TOKEN = Symbol('IExpenseCategoryRepository');
```

### Module Configuration

```typescript
import { Module } from '@nestjs/common';

@Module({
  providers: [
    // Use case implementations
    {
      provide: CREATE_EXPENSE_USE_CASE_TOKEN,
      useClass: CreateExpenseUseCase
    },
    {
      provide: GET_PAGED_EXPENSES_USE_CASE_TOKEN,
      useClass: GetPagedExpensesUseCase
    },
    {
      provide: GET_EXPENSE_BY_ID_USE_CASE_TOKEN,
      useClass: GetExpenseByIdUseCase
    },
    {
      provide: CREATE_EXPENSE_CATEGORY_USE_CASE_TOKEN,
      useClass: CreateExpenseCategoryUseCase
    },
    {
      provide: CREATE_MANY_EXPENSE_CATEGORIES_USE_CASE_TOKEN,
      useClass: CreateManyExpenseCategoriesUseCase
    },
    {
      provide: GET_PAGED_EXPENSE_CATEGORIES_USE_CASE_TOKEN,
      useClass: GetPagedExpenseCategoriesUseCase
    },
    
    // Repository implementations
    {
      provide: EXPENSE_REPOSITORY_TOKEN,
      useClass: ExpenseRepository
    },
    {
      provide: EXPENSE_CATEGORY_REPOSITORY_TOKEN,
      useClass: ExpenseCategoryRepository
    }
  ]
})
export class ExpenseModule {}
```

### Use Case Interfaces

```typescript
// Use case interfaces for type safety and testing
export interface ICreateExpenseUseCase {
  execute(expenseDto: ExpenseDto): Promise<ExpenseDto>;
}

export interface IGetPagedExpensesUseCase {
  execute(pagination: ExpensePaginationDto): Promise<PaginationResponseDto<ExpenseDto>>;
}

export interface IGetExpenseByIdUseCase {
  execute(id: string): Promise<ExpenseDto>;
}

export interface ICreateExpenseCategoryUseCase {
  execute(categoryDto: ExpenseCategoryDto): Promise<void>;
}

export interface ICreateManyExpenseCategoriesUseCase {
  execute(categoriesDto: ExpenseCategoryDto[]): Promise<void>;
}

export interface IGetPagedExpenseCategoriesUseCase {
  execute(pagination: PaginationRequestDto): Promise<PaginationResponseDto<ExpenseCategoryDto>>;
}
```

## Error Handling Patterns

### Common Error Types

```typescript
// Validation errors
export class ExpenseValidationError extends BadRequestException {
  constructor(message: string, field?: string) {
    super({
      message,
      field,
      type: 'EXPENSE_VALIDATION_ERROR'
    });
  }
}

// Business rule violations
export class ExpenseBusinessRuleError extends UnprocessableEntityException {
  constructor(message: string, rule?: string) {
    super({
      message,
      rule,
      type: 'EXPENSE_BUSINESS_RULE_ERROR'
    });
  }
}

// Resource not found
export class ExpenseNotFoundError extends NotFoundException {
  constructor(id: string) {
    super({
      message: `Expense with ID ${id} not found`,
      expenseId: id,
      type: 'EXPENSE_NOT_FOUND_ERROR'
    });
  }
}

// Conflict errors
export class ExpenseCategoryConflictError extends ConflictException {
  constructor(name: string) {
    super({
      message: `Expense category with name '${name}' already exists`,
      categoryName: name,
      type: 'EXPENSE_CATEGORY_CONFLICT_ERROR'
    });
  }
}
```

### Error Response Format

```typescript
// Standard error response structure
interface ErrorResponse {
  statusCode: number;
  message: string;
  error: string;
  type?: string;
  field?: string;
  rule?: string;
  timestamp: string;
  path: string;
}
```

## Transaction Management

### Database Transactions

```typescript
// Example of transactional use case
@Injectable()
export class CreateExpenseWithCategoriesUseCase {
  constructor(
    @Inject(EXPENSE_REPOSITORY_TOKEN)
    private readonly expenseRepository: IExpenseRepository,
    @Inject(EXPENSE_CATEGORY_REPOSITORY_TOKEN)
    private readonly categoryRepository: IExpenseCategoryRepository,
    private readonly databaseService: DatabaseService
  ) {}

  async execute(request: CreateExpenseWithCategoriesRequest): Promise<ExpenseDto> {
    return await this.databaseService.executeInTransaction(async (session) => {
      // Create categories if they don't exist
      const categories = await this.ensureCategoriesExist(
        request.newCategories,
        session
      );

      // Create expense with existing and new categories
      const expense = await this.createExpense(
        request.expense,
        categories,
        session
      );

      return expense;
    });
  }

  private async ensureCategoriesExist(
    newCategories: ExpenseCategoryDto[],
    session: DatabaseSession
  ): Promise<IExpenseCategory[]> {
    // Implementation with session-aware operations
    // ...
  }

  private async createExpense(
    expenseData: ExpenseDto,
    categories: IExpenseCategory[],
    session: DatabaseSession
  ): Promise<ExpenseDto> {
    // Implementation with session-aware operations
    // ...
  }
}
```

## Performance Considerations

### Caching Strategies

```typescript
// Category caching for frequently accessed data
@Injectable()
export class CachedGetExpenseCategoriesUseCase implements IGetPagedExpenseCategoriesUseCase {
  constructor(
    @Inject(GET_PAGED_EXPENSE_CATEGORIES_USE_CASE_TOKEN)
    private readonly baseUseCase: IGetPagedExpenseCategoriesUseCase,
    private readonly cacheService: CacheService
  ) {}

  async execute(
    pagination: PaginationRequestDto
  ): Promise<PaginationResponseDto<ExpenseCategoryDto>> {
    const cacheKey = `expense-categories:${pagination.page}:${pagination.size}`;
    
    const cached = await this.cacheService.get(cacheKey);
    if (cached) {
      return cached;
    }

    const result = await this.baseUseCase.execute(pagination);
    
    // Cache for 15 minutes (categories change infrequently)
    await this.cacheService.set(cacheKey, result, 900);
    
    return result;
  }
}
```

### Batch Operations

```typescript
// Optimized bulk operations
@Injectable()
export class BulkExpenseOperationsUseCase {
  constructor(
    @Inject(EXPENSE_REPOSITORY_TOKEN)
    private readonly expenseRepository: IExpenseRepository
  ) {}

  async createMultipleExpenses(expenses: ExpenseDto[]): Promise<ExpenseDto[]> {
    // Validate all expenses first
    expenses.forEach(this.validateExpense);

    // Batch create in chunks for better performance
    const chunkSize = 10;
    const chunks = this.chunkArray(expenses, chunkSize);
    
    const results: ExpenseDto[] = [];
    
    for (const chunk of chunks) {
      const chunkResults = await Promise.all(
        chunk.map(expense => this.createExpense(expense))
      );
      results.push(...chunkResults);
    }

    return results;
  }

  private chunkArray<T>(array: T[], chunkSize: number): T[][] {
    const chunks: T[][] = [];
    for (let i = 0; i < array.length; i += chunkSize) {
      chunks.push(array.slice(i, i + chunkSize));
    }
    return chunks;
  }
}
```

## Testing Patterns

### Use Case Testing

```typescript
describe('CreateExpenseUseCase', () => {
  let useCase: CreateExpenseUseCase;
  let mockRepository: jest.Mocked<IExpenseRepository>;

  beforeEach(() => {
    mockRepository = createMockExpenseRepository();
    useCase = new CreateExpenseUseCase(mockRepository);
  });

  describe('execute', () => {
    it('should create expense with valid data', async () => {
      // Arrange
      const expenseDto = ExpenseTestBuilder.createValid();
      const expectedExpense = ExpenseTestBuilder.createDomainFromDto(expenseDto);
      mockRepository.create.mockResolvedValue(expectedExpense);

      // Act
      const result = await useCase.execute(expenseDto);

      // Assert
      expect(mockRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          totalValue: expectedExpense.totalValue,
          expensedAt: expectedExpense.expensedAt
        })
      );
      expect(result).toEqual(ExpenseMapperToDto.map(expectedExpense));
    });

    it('should throw error for negative amount', async () => {
      // Arrange
      const expenseDto = ExpenseTestBuilder.createWithAmount(-100);

      // Act & Assert
      await expect(useCase.execute(expenseDto)).rejects.toThrow(
        'Expense amount must be greater than zero'
      );
    });

    it('should validate currency consistency across items', async () => {
      // Arrange
      const expenseDto = ExpenseTestBuilder.createWithMixedCurrencies();

      // Act & Assert
      await expect(useCase.execute(expenseDto)).rejects.toThrow(
        'All expense items must use the same currency'
      );
    });
  });
});
```

### Integration Testing

```typescript
describe('Expense Management Integration', () => {
  let app: TestingModule;
  let useCase: CreateExpenseUseCase;
  let repository: IExpenseRepository;

  beforeAll(async () => {
    app = await Test.createTestingModule({
      imports: [ExpenseModule, DatabaseTestModule],
      providers: [
        // Test-specific providers
      ]
    }).compile();

    useCase = app.get(CREATE_EXPENSE_USE_CASE_TOKEN);
    repository = app.get(EXPENSE_REPOSITORY_TOKEN);
  });

  afterEach(async () => {
    await repository.deleteAll(); // Clean up test data
  });

  it('should create expense end-to-end', async () => {
    // Arrange
    const expenseDto = ExpenseTestBuilder.createValid();

    // Act
    const result = await useCase.execute(expenseDto);

    // Assert
    expect(result._id).toBeDefined();
    
    const savedExpense = await repository.findOneById(result._id!);
    expect(savedExpense).toBeDefined();
    expect(savedExpense!.totalValue.amount).toBe(expenseDto.totalValue.amount);
  });
});
```

## Monitoring and Observability

### Use Case Metrics

```typescript
// Decorator for use case monitoring
export function MonitorUseCase(name: string) {
  return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
    const method = descriptor.value;

    descriptor.value = async function (...args: any[]) {
      const startTime = Date.now();
      const logger = this.logger || console;

      try {
        logger.log(`Starting use case: ${name}`);
        const result = await method.apply(this, args);
        const duration = Date.now() - startTime;
        
        // Log successful execution
        logger.log(`Use case ${name} completed in ${duration}ms`);
        
        // Send metrics to monitoring system
        this.metricsService?.recordUseCaseExecution(name, duration, 'success');
        
        return result;
      } catch (error) {
        const duration = Date.now() - startTime;
        
        // Log error
        logger.error(`Use case ${name} failed after ${duration}ms:`, error);
        
        // Send error metrics
        this.metricsService?.recordUseCaseExecution(name, duration, 'error');
        
        throw error;
      }
    };
  };
}

// Usage in use case
@Injectable()
export class CreateExpenseUseCase implements ICreateExpenseUseCase {
  @MonitorUseCase('CreateExpense')
  async execute(expenseDto: ExpenseDto): Promise<ExpenseDto> {
    // Implementation
  }
}
```

### Business Event Logging

```typescript
// Event logging for business operations
@Injectable()
export class CreateExpenseUseCase implements ICreateExpenseUseCase {
  constructor(
    @Inject(EXPENSE_REPOSITORY_TOKEN)
    private readonly expenseRepository: IExpenseRepository,
    private readonly eventLogger: BusinessEventLogger
  ) {}

  async execute(expenseDto: ExpenseDto): Promise<ExpenseDto> {
    // Implementation

    // Log business event
    await this.eventLogger.logEvent({
      type: 'EXPENSE_CREATED',
      entityId: result._id,
      entityType: 'Expense',
      amount: result.totalValue.amount,
      currency: result.totalValue.currency,
      categories: result.items?.flatMap(item => item.categories.map(cat => cat.name)) || [],
      timestamp: new Date(),
      userId: this.getCurrentUserId(),
      tenantId: this.getCurrentTenantId()
    });

    return result;
  }
}
```

This comprehensive use case documentation provides detailed insights into the business operations of the expense management system, including validation rules, error handling, performance considerations, and testing patterns. Each use case is designed to be maintainable, testable, and aligned with Clean Architecture principles.
