IController Integration Plan for SMS-CO2 Project

1. Executive Summary

This document outlines the comprehensive plan for integrating a standardized controller architecture into the SMS-CO2 project. The implementation of the IController interface will establish consistent API endpoints and response formats, improving maintainability and developer experience across the application.

2. Current Architecture Analysis

The SMS-CO2 project follows a domain-driven design with a clear separation of concerns. The current controller implementation lacks standardization, with routes being defined separately from controllers.

2.1 Current Directory Structure

F:\Github\Capstone_Xin\sms-co2\co2-emission-system\
├── packages
│   ├── backend
│   │   ├── src
│   │   │   ├── app.ts
│   │   │   ├── server.ts
│   │   │   ├── domain
│   │   │   ├── infrastructure
│   │   │   └── interfaces
│   │   │       └── http
│   │   │           ├── controllers
│   │   │           │   └── emissionController.ts
│   │   │           ├── middlewares
│   │   │           │   └── errorHandler.ts
│   │   │           └── routes
│   │   │               └── emissionRoutes.ts
│   ├── frontend
│   └── shared

2.2 Existing Controller Implementation

The current controller pattern:

Example from existing code:

// Current EmissionController implementation
export class EmissionController {
  constructor(private emissionService: EmissionService) {}

  async createEmission(req: Request, res: Response): Promise<void> {
    try {
      // Implementation details
      res.status(201).json({ 
        success: true, 
        message: 'Emission created successfully' 
      });
    } catch (error) {
      // Error handling
    }
  }

  // Other methods...
}

Route registration:

// Current route registration
const router = Router();
const emissionController = new EmissionController(emissionService);

router.post('/', (req, res) => emissionController.createEmission(req, res));
router.get('/:id', (req, res) => emissionController.getEmissionById(req, res));
// Other routes...

3. Implementation Plan

Phase 1: Infrastructure Setup (3 days)

The first phase establishes the foundation for the standardized controller architecture.

3.1.1 Create IController Interface

// src/interfaces/http/controllers/base/IController.ts
import { Router } from 'express';

export interface IController {
  // Base path for controller routes
  path: string;

  // Express router instance
  router: Router;

  // Initialize and register routes
  initializeRoutes(): void;
}

3.1.2 Implement BaseController Class

// src/interfaces/http/controllers/base/BaseController.ts
import { Router, Request, Response, NextFunction } from 'express';
import { IController } from './IController';

export abstract class BaseController implements IController {
  public path: string;
  public router: Router;

  constructor(path: string) {
    this.path = path;
    this.router = Router();
    this.initializeRoutes();
  }

  // Must be implemented by derived controllers
  abstract initializeRoutes(): void;

  // Standard response formats
  protected sendSuccess(res: Response, data: any, statusCode: number = 200): Response {
    return res.status(statusCode).json({
      success: true,
      data,
    });
  }

  protected sendError(
    res: Response, 
    message: string, 
    statusCode: number = 400, 
    errors?: any
  ): Response {
    return res.status(statusCode).json({
      success: false,
      message,
      errors,
    });
  }

  // Pagination support
  protected sendPaginated(
    res: Response,
    data: any[],
    page: number,
    limit: number,
    total: number,
    statusCode: number = 200
  ): Response {
    return res.status(statusCode).json({
      success: true,
      data,
      pagination: {
        page,
        limit,
        total,
        pages: Math.ceil(total / limit),
      },
    });
  }

  // Error handling wrapper for async methods
  protected catchAsync(fn: (req: Request, res: Response, next: NextFunction) => Promise<any>) {
    return (req: Request, res: Response, next: NextFunction) => {
      fn(req, res, next).catch(next);
    };
  }
}

3.1.3 Update Error Handling Middleware

Ensure the error handling middleware works with the new controller pattern:

// src/interfaces/http/middlewares/errorHandler.ts
import { Request, Response, NextFunction } from 'express';

export class HttpException extends Error {
  status: number;
  message: string;

  constructor(status: number, message: string) {
    super(message);
    this.status = status;
    this.message = message;
  }
}

export const errorHandler = (
  error: Error,
  req: Request,
  res: Response,
  next: NextFunction
): void => {
  const status = error instanceof HttpException ? error.status : 500;
  const message = error.message || 'Something went wrong';

  res.status(status).json({
    success: false,
    message,
    stack: process.env.NODE_ENV === 'development' ? error.stack : undefined,
  });
};

Phase 2: Controller Migration (5 days)

3.2.1 Refactor EmissionController

// src/interfaces/http/controllers/emissionController.ts
import { Request, Response } from 'express';
import { BaseController } from './base/BaseController';
import { EmissionService } from '../../../domain/services/emissionService';
import { CreateEmissionDto } from '../../../domain/models/emission';
import multer from 'multer';

export class EmissionController extends BaseController {
  private upload = multer({ storage: multer.memoryStorage() });

  constructor(private emissionService: EmissionService) {
    super('/api/emissions');
  }

  initializeRoutes(): void {
    // Register all routes with their handlers
    this.router.post(this.path, this.catchAsync(this.createEmission.bind(this)));
    this.router.get(`${this.path}/:id`, this.catchAsync(this.getEmissionById.bind(this)));
    this.router.get(this.path, this.catchAsync(this.getAllEmissions.bind(this)));
    this.router.post(
      `${this.path}/import`, 
      this.upload.single('file'), 
      this.catchAsync(this.importEmissions.bind(this))
    );
  }

  // Controller methods - refactored to use standardized responses
  private async createEmission(req: Request, res: Response): Promise<void> {
    // Parse request body to CreateEmissionDto
    var finaldate = new Date(req.body.date);
    const utcDate = new Date(finaldate.getUTCFullYear(), finaldate.getUTCMonth(), finaldate.getUTCDate());

    const emissionData: CreateEmissionDto = {
      contributorId: parseInt(req.body.contributorId),
      date: utcDate,
      location: req.body.location,
      value: parseFloat(req.body.value),
      unit: req.body.unit,
      emission_type: req.body.type,
      hours: parseFloat(req.body.hours)
    };

    // Call service to create the emission
    const success = await this.emissionService.createEmission(emissionData);

    if (success) {
      this.sendSuccess(res, { message: 'Emission created successfully' }, 201);
    } else {
      this.sendError(res, 'Failed to create emission', 500);
    }
  }

  private async getEmissionById(req: Request, res: Response): Promise<void> {
    const id = parseInt(req.params.id);

    if (isNaN(id)) {
      return this.sendError(res, 'Invalid emission ID', 400);
    }

    const emission = await this.emissionService.getEmissionById(id);

    if (!emission) {
      return this.sendError(res, 'Emission not found', 404);
    }

    this.sendSuccess(res, emission);
  }

  private async getAllEmissions(req: Request, res: Response): Promise<void> {
    const emissions = await this.emissionService.getAllEmissions();
    this.sendSuccess(res, emissions);
  }

  private async importEmissions(req: Request, res: Response): Promise<void> {
    // Check if file exists in request
    if (!req.file) {
      return this.sendError(res, 'No file uploaded', 400);
    }

    // Get file buffer and process it
    const csvBuffer = req.file.buffer;
    const importResult = await this.emissionService.importEmissionsFromCsv(csvBuffer);

    // Respond with results
    if (importResult.success) {
      this.sendSuccess(res, {
        message: 'CSV imported successfully',
        totalProcessed: importResult.totalProcessed,
        totalSucceeded: importResult.totalSucceeded,
        totalFailed: importResult.totalFailed
      });
    } else {
      this.sendError(res, 'CSV validation failed', 400, {
        errors: importResult.errors,
        validRecords: importResult.validRecords,
        totalProcessed: importResult.totalProcessed
      });
    }
  }
}

3.2.2 Update Application Bootstrap

Update app.ts to use the new controller pattern:

// src/app.ts
import express from 'express';
import cors from 'cors';
import { errorHandler } from './interfaces/http/middlewares/errorHandler';
import { EmissionController } from './interfaces/http/controllers/emissionController';
import { EmissionService } from './domain/services/emissionService';
import { EmissionRepository } from './infrastructure/database/repositories/emissionRepository';
import { IController } from './interfaces/http/controllers/base/IController';

export class App {
  public app: express.Application;

  constructor(controllers: IController[]) {
    this.app = express();
    this.initializeMiddlewares();
    this.initializeControllers(controllers);
    this.initializeErrorHandling();
  }

  private initializeMiddlewares(): void {
    this.app.use(cors());
    this.app.use(express.json());
    this.app.use(express.urlencoded({ extended: true }));
  }

  private initializeControllers(controllers: IController[]): void {
    controllers.forEach((controller) => {
      this.app.use('/', controller.router);
    });
  }

  private initializeErrorHandling(): void {
    this.app.use(errorHandler);
  }

  // Health check endpoint
  private initializeHealthCheck(): void {
    this.app.get('/health', (req, res) => {
      res.status(200).json({ status: 'ok' });
    });
  }
}

// Initialize repositories and services
const emissionRepository = new EmissionRepository();
const emissionService = new EmissionService(emissionRepository);

// Initialize controllers
const controllers: IController[] = [
  new EmissionController(emissionService),
  // Add other controllers here
];

// Create app instance
const app = new App(controllers).app;

export default app;

3.2.3 Update Server.ts

// src/server.ts
import app from './app';
import config from './config';

const PORT = config.server.port || 3000;

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Phase 3: Testing & Documentation (4 days)

3.3.1 Create Controller Tests

// tests/unit/controllers/baseController.test.ts
import { BaseController } from '../../../src/interfaces/http/controllers/base/BaseController';
import { Request, Response } from 'express';

// Test implementation class
class TestController extends BaseController {
  constructor() {
    super('/test');
  }

  initializeRoutes(): void {
    // Empty for testing purposes
  }

  // Expose protected methods for testing
  public testSendSuccess(res: Response, data: any, statusCode?: number): Response {
    return this.sendSuccess(res, data, statusCode);
  }

  public testSendError(res: Response, message: string, statusCode?: number, errors?: any): Response {
    return this.sendError(res, message, statusCode, errors);
  }
}

describe('BaseController', () => {
  let controller: TestController;
  let mockRequest: Partial<Request>;
  let mockResponse: Partial<Response>;

  beforeEach(() => {
    controller = new TestController();
    mockRequest = {};
    mockResponse = {
      status: jest.fn().mockReturnThis(),
      json: jest.fn().mockReturnThis(),
    };
  });

  describe('sendSuccess', () => {
    it('should return success response with status 200 by default', () => {
      const data = { id: 1, name: 'Test' };
      controller.testSendSuccess(mockResponse as Response, data);

      expect(mockResponse.status).toHaveBeenCalledWith(200);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: true,
        data
      });
    });

    it('should return success response with custom status code', () => {
      const data = { id: 1, name: 'Test' };
      controller.testSendSuccess(mockResponse as Response, data, 201);

      expect(mockResponse.status).toHaveBeenCalledWith(201);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: true,
        data
      });
    });
  });

  describe('sendError', () => {
    it('should return error response with status 400 by default', () => {
      const message = 'Test error';
      controller.testSendError(mockResponse as Response, message);

      expect(mockResponse.status).toHaveBeenCalledWith(400);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: false,
        message
      });
    });

    it('should return error response with custom status code and errors', () => {
      const message = 'Test error';
      const errors = { field: 'Invalid value' };
      controller.testSendError(mockResponse as Response, message, 422, errors);

      expect(mockResponse.status).toHaveBeenCalledWith(422);
      expect(mockResponse.json).toHaveBeenCalledWith({
        success: false,
        message,
        errors
      });
    });
  });
});

3.3.2 Documentation

Add JSDoc comments to all controller methods and interfaces, for example:

/**
 * Interface for standardizing controller implementation
 * across the application
 */
export interface IController {
  /**
   * Base path for the controller routes
   */
  path: string;

  /**
   * Express router instance for registering routes
   */
  router: Router;

  /**
   * Initialize and register all routes for this controller
   */
  initializeRoutes(): void;
}

Phase 4: Further Improvements (3 days)

3.4.1 Controller Factory Implementation

// src/interfaces/http/controllers/factory/controllerFactory.ts
import { IController } from '../base/IController';
import { EmissionController } from '../emissionController';
import { EmissionService } from '../../../../domain/services/emissionService';
import { EmissionRepository } from '../../../../infrastructure/database/repositories/emissionRepository';

export class ControllerFactory {
  static createControllers(): IController[] {
    // Create repositories
    const emissionRepository = new EmissionRepository();

    // Create services with repositories
    const emissionService = new EmissionService(emissionRepository);

    // Create controllers with services
    return [
      new EmissionController(emissionService),
      // Add other controllers as needed
    ];
  }
}

Update app.ts to use the factory:

// src/app.ts
import { ControllerFactory } from './interfaces/http/controllers/factory/controllerFactory';

// Create app instance
const controllers = ControllerFactory.createControllers();
const app = new App(controllers).app;

3.4.2 Swagger Documentation

Add Swagger annotations to controller methods:

/**
 * @swagger
 * /api/emissions:
 *   get:
 *     summary: Get all emissions
 *     tags: [Emissions]
 *     responses:
 *       200:
 *         description: List of emissions
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *                 data:
 *                   type: array
 *                   items:
 *                     $ref: '#/components/schemas/Emission'
 */
private async getAllEmissions(req: Request, res: Response): Promise<void> {
  const emissions = await this.emissionService.getAllEmissions();
  this.sendSuccess(res, emissions);
}

4. Implementation Timeline

Phase Task Duration Dependencies
1. Infrastructure Create IController Interface 0.5 days None
Implement BaseController 1.5 days IController
Update Error Handling 1 day BaseController
2. Controller Migration Refactor EmissionController 2 days Phase 1
Update App Bootstrap 1 day Refactored Controller
Update Server.ts 0.5 days Updated App
Manual Testing 1.5 days Phase 2
3. Testing & Documentation Create Unit Tests 2 days Phase 2
Add JSDoc Comments 1 day None
Test Coverage Analysis 1 day Unit Tests
4. Further Improvements Implement Controller Factory 1 day Phase 3
Add Swagger Documentation 2 days None

Total Duration: 15 working days

5. Risks and Mitigation

Risk Impact Probability Mitigation
Incompatible existing controllers High Medium Create adapter pattern for legacy controllers
Regression in API behavior High Low Comprehensive test suite before/after changes
Extended development timeline Medium Medium Phased approach allows partial implementation
Inconsistent adoption Medium Low Code review process to enforce new pattern

6. Conclusion

The standardization of controllers through the IController interface will significantly improve the maintainability, consistency, and developer experience of the SMS-CO2 application. This refactoring focuses on enhancing the structure without disrupting functionality, allowing for a smooth transition to the new architecture.

By following this phased approach over a 15-day period, we can implement these changes methodically while minimizing risk to the existing application. The resulting controller standardization will provide a solid foundation for future development and scaling of the application.