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.

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.

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.

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.

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.

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.

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

Module Configuration

Use Case Interfaces

Error Handling Patterns

Common Error Types

Error Response Format

Transaction Management

Database Transactions

Performance Considerations

Caching Strategies

Batch Operations

Testing Patterns

Use Case Testing

Integration Testing

Monitoring and Observability

Use Case Metrics

Business Event Logging

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.

Last updated

Was this helpful?