express-api/src/services/buildings/buildingServices.ts

Summary

Maintainability
A
30 mins
Test Coverage
import { Building } from '@/typeorm/Entities/Building';
import { AppDataSource } from '@/appDataSource';
import { ErrorWithCode } from '@/utilities/customErrors/ErrorWithCode';
import { DeepPartial, FindOptionsOrder, In } from 'typeorm';
import { BuildingFilter } from '@/services/buildings/buildingSchema';
import userServices from '../users/usersServices';
import { BuildingEvaluation } from '@/typeorm/Entities/BuildingEvaluation';
import { BuildingFiscal } from '@/typeorm/Entities/BuildingFiscal';
import logger from '@/utilities/winstonLogger';
import { ProjectProperty } from '@/typeorm/Entities/ProjectProperty';
import { Roles } from '@/constants/roles';
import { PimsRequestUser } from '@/middleware/userAuthCheck';
import { User } from '@/typeorm/Entities/User';

const buildingRepo = AppDataSource.getRepository(Building);

/**
 * @description          Adds a new building to the datasource.
 * @param   building     Incoming building data to be added to the database.
 * @returns {Response}   New Building added.
 * @throws {ErrorWithCode} If the building already exists or is unable to be added.
 */
export const addBuilding = async (building: DeepPartial<Building>) => {
  // TODO: Why would an incoming building have an id? Is there a better way to check for existing buildings?
  const existingBuilding = building.Id ? await getBuildingById(building.Id) : null;
  if (existingBuilding) {
    throw new ErrorWithCode('Building already exists.', 409);
  }
  const newBuilding = await buildingRepo.save(building);
  return newBuilding;
};

/**
 * @description Finds and returns a building with matching Id
 * @param       buildingId  - Number representing building we want to find.
 * @returns     findBuilding - Building data matching Id passed in.
 */
export const getBuildingById = async (buildingId: number) => {
  const evaluations = await AppDataSource.getRepository(BuildingEvaluation).find({
    where: { BuildingId: buildingId, EvaluationKeyId: 0 },
    order: { Year: 'DESC' },
  });
  const fiscals = await AppDataSource.getRepository(BuildingFiscal).find({
    where: { BuildingId: buildingId },
    order: { FiscalYear: 'DESC' },
  });
  const findBuilding = await buildingRepo.findOne({
    where: { Id: buildingId },
  });
  if (findBuilding) {
    return {
      ...findBuilding,
      Evaluations: evaluations,
      Fiscals: fiscals,
    };
  } else {
    return null;
  }
};

/**
 * @description Update a building with matching Id
 * @param       {DeepPartial<Building>} building  A partial building with updated info.
 * @returns     {Building} The updated building
 * @throws      {ErrorWithCode} Throws and error with 404 status if building does not exist.
 */
export const updateBuildingById = async (
  building: DeepPartial<Building>,
  user: PimsRequestUser,
) => {
  const existingBuilding = await getBuildingById(building.Id);
  if (!existingBuilding) {
    throw new ErrorWithCode('Building does not exists.', 404);
  }
  const validUserAgencies = await userServices.getAgencies(user.Username);
  const isAdmin = user.hasOneOfRoles([Roles.ADMIN]);
  if (!isAdmin && !validUserAgencies.includes(building.AgencyId)) {
    throw new ErrorWithCode('This agency change is not permitted.', 403);
  }
  if (building.Fiscals && building.Fiscals.length) {
    building.Fiscals = await Promise.all(
      building.Fiscals.map(async (fiscal) => {
        const exists = await AppDataSource.getRepository(BuildingFiscal).findOne({
          where: {
            BuildingId: building.Id,
            FiscalYear: fiscal.FiscalYear,
            FiscalKeyId: fiscal.FiscalKeyId,
          },
        });
        const fiscalEntity: DeepPartial<BuildingFiscal> = {
          ...fiscal,
          CreatedById: exists ? exists.CreatedById : building.UpdatedById,
          UpdatedById: exists ? building.UpdatedById : undefined,
        };
        return fiscalEntity;
      }),
    );
  }
  if (building.Evaluations && building.Evaluations.length) {
    building.Evaluations = await Promise.all(
      building.Evaluations.map(async (evaluation) => {
        const exists = await AppDataSource.getRepository(BuildingEvaluation).findOne({
          where: {
            BuildingId: building.Id,
            Year: evaluation.Year,
            EvaluationKeyId: evaluation.EvaluationKeyId,
          },
        });
        const evaluationEntity: DeepPartial<BuildingEvaluation> = {
          ...evaluation,
          CreatedById: exists ? exists.CreatedById : building.UpdatedById,
          UpdatedById: exists ? building.UpdatedById : undefined,
        };
        return evaluationEntity;
      }),
    );
  }

  const updatedBuilding = await buildingRepo.save(building);
  return updatedBuilding;
};

/**
 * Deletes a building by its ID after checking for its existence and linked projects.
 * Throws an error if the building does not exist or is linked to projects.
 * Updates the building, its evaluations, and fiscals with deletion information in a transaction.
 * @param {number}  buildingId The ID of the building to delete.
 * @param {string}  username The username of the user performing the deletion.
 * @returns A promise that resolves to the removed building entity.
 * @throws Error if the building does not exist, is linked to projects, or an error occurs during the deletion process.
 */
export const deleteBuildingById = async (buildingId: number, user: User) => {
  const existingBuilding = await getBuildingById(buildingId);
  if (!existingBuilding) {
    throw new ErrorWithCode('Building does not exists.', 404);
  }
  const linkedProjects = await AppDataSource.getRepository(ProjectProperty).find({
    where: { BuildingId: buildingId },
  });
  if (linkedProjects.length) {
    throw new ErrorWithCode(
      `Building is involved in one or more projects with ID(s) ${linkedProjects.map((proj) => proj.ProjectId).join(', ')}`,
      403,
    );
  }
  const queryRunner = await AppDataSource.createQueryRunner();
  await queryRunner.startTransaction();
  try {
    const removeBuilding = await queryRunner.manager.update(Building, existingBuilding.Id, {
      DeletedById: user.Id,
      DeletedOn: new Date(),
    });
    await queryRunner.manager.update(
      BuildingEvaluation,
      { BuildingId: existingBuilding.Id },
      {
        DeletedById: user.Id,
        DeletedOn: new Date(),
      },
    );
    await queryRunner.manager.update(
      BuildingFiscal,
      { BuildingId: existingBuilding.Id },
      {
        DeletedById: user.Id,
        DeletedOn: new Date(),
      },
    );
    await queryRunner.commitTransaction();
    return removeBuilding;
  } catch (e) {
    await queryRunner.rollbackTransaction();
    logger.warn(e.message);
    if (e instanceof ErrorWithCode) throw e;
    throw new ErrorWithCode(`Error updating building: ${e.message}`, 500);
  } finally {
    await queryRunner.release();
  }
};

/**
 * @description Retrieves buildings based on the provided filter.
 * @param {BuildingFilter} filter - The filter object used to specify the criteria for retrieving buildings.
 * @param {boolean} includeRelations Boolean that controls if related tables should be joined.
 * @returns {Building[]} An array of buildings that match the filter criteria.
 */
export const getBuildings = async (filter: BuildingFilter, includeRelations: boolean = false) => {
  const buildings = await buildingRepo.find({
    relations: {
      Agency: {
        Parent: includeRelations,
      },
      AdministrativeArea: includeRelations,
      Classification: includeRelations,
      PropertyType: includeRelations,
      Evaluations: includeRelations,
    },
    select: {
      Agency: {
        Id: true,
        Name: true,
        Parent: {
          Id: true,
          Name: true,
        },
      },
      AdministrativeArea: {
        Id: true,
        Name: true,
      },
      Classification: {
        Id: true,
        Name: true,
      },
      PropertyType: {
        Id: true,
        Name: true,
      },
      Evaluations: {
        EvaluationKeyId: true,
        Year: true,
        Value: true,
      },
    },
    where: {
      PID: filter.pid,
      ClassificationId: filter.classificationId,
      AgencyId: filter.agencyId
        ? In(typeof filter.agencyId === 'number' ? [filter.agencyId] : filter.agencyId)
        : undefined,
      AdministrativeAreaId: filter.administrativeAreaId,
      PropertyTypeId: filter.propertyTypeId,
      BuildingConstructionTypeId: filter.buildingConstructionTypeId,
      BuildingPredominateUseId: filter.buildingPredominateUseId,
      BuildingOccupantTypeId: filter.buildingOccupantTypeId,
      IsSensitive: filter.isSensitive,
    },
    take: filter.quantity,
    skip: (filter.page ?? 0) * (filter.quantity ?? 0),
    order: filter.sort as FindOptionsOrder<Building>,
  });
  return buildings;
};