Skip to content

Application Services

Application services orchestrate the execution of business use cases. They coordinate the work of domain objects and infrastructure services to fulfill the needs of the application.

Current State

In the current codebase, services contain a mix of:

  • Business logic that should be in domain entities
  • Orchestration logic
  • Infrastructure concerns
  • Transaction management

Example from the current codebase:

JavaScript
// src/app/services/requisitionService.js
class RequisitionService {
  constructor(container) {
    // ... dependency injection
  }

  async createRequisition(request, reply) {
    // ... validation and setup

    try {
      requisition = await this.requisitionRepository.create(body, {
        transaction,
      });

      const rsApprovers = await this.requisitionService.rsApproversV2({
        category,
        projectId,
        departmentId,
        userFromToken,
        transaction,
        companyId,
      });

      await this.requisitionService.assignRSApprovers({
        rsApprovers,
        category,
        requisitionId: requisition.id,
        transaction,
      });

      await transaction.commit();
      // ... more logic
    } catch (error) {
      await transaction.rollback();
      throw this.clientErrors.BAD_REQUEST({
        message: 'Requisition creation failed',
      });
    }
  }
}

Improved Application Services

Application services should:

  1. Orchestrate domain objects and infrastructure services
  2. Handle use cases
  3. Manage transactions
  4. Not contain business logic (that should be in domain entities)
  5. Be thin and focused on coordination

Example: RequisitionApplicationService

JavaScript
// src/application/requisitionManagement/RequisitionApplicationService.js
class RequisitionApplicationService {
  constructor({
    requisitionRepository,
    userRepository,
    departmentRepository,
    projectRepository,
    companyRepository,
    eventBus,
    unitOfWork
  }) {
    this.requisitionRepository = requisitionRepository;
    this.userRepository = userRepository;
    this.departmentRepository = departmentRepository;
    this.projectRepository = projectRepository;
    this.companyRepository = companyRepository;
    this.eventBus = eventBus;
    this.unitOfWork = unitOfWork;
  }

  /**
   * Create a new requisition
   * @param {Object} data - The requisition data
   * @param {number} userId - The ID of the user creating the requisition
   * @returns {Promise<Requisition>} - The created requisition
   */
  async createRequisition(data, userId) {
    return this.unitOfWork.execute(async () => {
      // Validate references
      const department = await this.departmentRepository.findById(data.departmentId);
      if (!department) {
        throw new Error('Department not found');
      }

      if (data.projectId) {
        const project = await this.projectRepository.findById(data.projectId);
        if (!project) {
          throw new Error('Project not found');
        }
      }

      const company = await this.companyRepository.findById(data.companyId);
      if (!company) {
        throw new Error('Company not found');
      }

      // Create domain entity using factory method
      const requisition = Requisition.create({
        ...data,
        createdBy: userId,
        companyCode: company.code,
      });

      // Add items if provided
      if (data.items && Array.isArray(data.items)) {
        for (const itemData of data.items) {
          const item = new RequisitionItem(itemData);
          requisition.addItem(item);
        }
      }

      // Determine approvers
      const approvers = await this._determineApprovers({
        category: data.category,
        projectId: data.projectId,
        departmentId: data.departmentId,
        userId,
        companyId: data.companyId,
      });

      // Add approvers to requisition
      for (const approverData of approvers) {
        const approver = new RequisitionApprover(approverData);
        requisition.addApprover(approver);
      }

      // Save to repository
      const savedRequisition = await this.requisitionRepository.save(requisition);

      // Publish domain event
      this.eventBus.publish(new RequisitionCreatedEvent(savedRequisition));

      return savedRequisition;
    });
  }

  /**
   * Submit a requisition for approval
   * @param {number} requisitionId - The requisition ID
   * @param {number} userId - The ID of the user submitting the requisition
   * @returns {Promise<Requisition>} - The submitted requisition
   */
  async submitRequisition(requisitionId, userId) {
    return this.unitOfWork.execute(async () => {
      // Get requisition from repository
      const requisition = await this.requisitionRepository.findById(requisitionId);
      if (!requisition) {
        throw new Error('Requisition not found');
      }

      // Check if user is authorized to submit
      if (requisition.createdBy !== userId) {
        throw new Error('Only the creator can submit a requisition');
      }

      // Execute domain logic
      const event = requisition.submit();

      // Save changes
      const savedRequisition = await this.requisitionRepository.save(requisition);

      // Publish event
      if (event) {
        this.eventBus.publish(event);
      }

      return savedRequisition;
    });
  }

  /**
   * Approve a requisition
   * @param {number} requisitionId - The requisition ID
   * @param {number} approverId - The ID of the approver
   * @returns {Promise<Requisition>} - The approved requisition
   */
  async approveRequisition(requisitionId, approverId) {
    return this.unitOfWork.execute(async () => {
      // Get requisition from repository
      const requisition = await this.requisitionRepository.findById(requisitionId);
      if (!requisition) {
        throw new Error('Requisition not found');
      }

      // Execute domain logic
      const event = requisition.approve(approverId);

      // Save changes
      const savedRequisition = await this.requisitionRepository.save(requisition);

      // Publish event if any
      if (event) {
        this.eventBus.publish(event);
      }

      return savedRequisition;
    });
  }

  /**
   * Reject a requisition
   * @param {number} requisitionId - The requisition ID
   * @param {number} approverId - The ID of the approver
   * @param {string} reason - The reason for rejection
   * @returns {Promise<Requisition>} - The rejected requisition
   */
  async rejectRequisition(requisitionId, approverId, reason) {
    return this.unitOfWork.execute(async () => {
      // Get requisition from repository
      const requisition = await this.requisitionRepository.findById(requisitionId);
      if (!requisition) {
        throw new Error('Requisition not found');
      }

      // Execute domain logic
      const event = requisition.reject(approverId, reason);

      // Save changes
      const savedRequisition = await this.requisitionRepository.save(requisition);

      // Publish event
      if (event) {
        this.eventBus.publish(event);
      }

      return savedRequisition;
    });
  }

  /**
   * Get requisitions by status
   * @param {string} status - The status to filter by
   * @param {Object} options - Additional options (pagination, etc.)
   * @returns {Promise<{data: Requisition[], total: number}>} - The requisitions and total count
   */
  async getRequisitionsByStatus(status, options = {}) {
    return this.requisitionRepository.findByStatus(status, options);
  }

  /**
   * Get requisitions created by a user
   * @param {number} userId - The user ID
   * @param {Object} options - Additional options (pagination, etc.)
   * @returns {Promise<{data: Requisition[], total: number}>} - The requisitions and total count
   */
  async getRequisitionsByCreator(userId, options = {}) {
    return this.requisitionRepository.findByCreator(userId, options);
  }

  /**
   * Get a requisition by ID
   * @param {number} id - The requisition ID
   * @returns {Promise<Requisition|null>} - The requisition or null if not found
   */
  async getRequisitionById(id) {
    return this.requisitionRepository.findById(id);
  }

  // Private methods
  async _determineApprovers({ category, projectId, departmentId, userId, companyId }) {
    // Implementation of approver determination logic
    // This would replace the current rsApproversV2 method
    // ...
  }
}

module.exports = RequisitionApplicationService;

Refactoring Controllers

Controllers should be thin adapters that translate between the HTTP layer and application services:

JavaScript
// src/interfaces/api/controllers/RequisitionController.js
class RequisitionController {
  constructor({ requisitionApplicationService }) {
    this.requisitionService = requisitionApplicationService;
  }

  async createRequisition(request, reply) {
    try {
      const { body, userFromToken } = request;

      const requisition = await this.requisitionService.createRequisition(
        body,
        userFromToken.id
      );

      return reply.status(201).send(requisition);
    } catch (error) {
      // Handle errors
      return reply.status(400).send({ error: error.message });
    }
  }

  async submitRequisition(request, reply) {
    try {
      const { id } = request.params;
      const { userFromToken } = request;

      const requisition = await this.requisitionService.submitRequisition(
        parseInt(id),
        userFromToken.id
      );

      return reply.status(200).send(requisition);
    } catch (error) {
      // Handle errors
      return reply.status(400).send({ error: error.message });
    }
  }

  async approveRequisition(request, reply) {
    try {
      const { id } = request.params;
      const { userFromToken } = request;

      const requisition = await this.requisitionService.approveRequisition(
        parseInt(id),
        userFromToken.id
      );

      return reply.status(200).send(requisition);
    } catch (error) {
      // Handle errors
      return reply.status(400).send({ error: error.message });
    }
  }

  async rejectRequisition(request, reply) {
    try {
      const { id } = request.params;
      const { userFromToken, body } = request;

      const requisition = await this.requisitionService.rejectRequisition(
        parseInt(id),
        userFromToken.id,
        body.reason
      );

      return reply.status(200).send(requisition);
    } catch (error) {
      // Handle errors
      return reply.status(400).send({ error: error.message });
    }
  }

  async getRequisitions(request, reply) {
    try {
      const { query } = request;
      const { status, page, limit } = query;

      const requisitions = await this.requisitionService.getRequisitionsByStatus(
        status,
        { page: parseInt(page) || 1, limit: parseInt(limit) || 10 }
      );

      return reply.status(200).send(requisitions);
    } catch (error) {
      // Handle errors
      return reply.status(400).send({ error: error.message });
    }
  }

  async getRequisitionById(request, reply) {
    try {
      const { id } = request.params;

      const requisition = await this.requisitionService.getRequisitionById(parseInt(id));

      if (!requisition) {
        return reply.status(404).send({ error: 'Requisition not found' });
      }

      return reply.status(200).send(requisition);
    } catch (error) {
      // Handle errors
      return reply.status(400).send({ error: error.message });
    }
  }
}

module.exports = RequisitionController;