Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PIMS-1871: Fuzzy search is returning properties already in other projects #2590

Merged
merged 9 commits into from
Aug 2, 2024
52 changes: 45 additions & 7 deletions express-api/src/services/properties/propertiesServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ import userServices from '../users/usersServices';
import { Brackets, FindManyOptions, FindOptionsWhere, ILike, In, QueryRunner } from 'typeorm';
import { SSOUser } from '@bcgov/citz-imb-sso-express';
import { PropertyType } from '@/constants/propertyType';
import { ProjectStatus } from '@/constants/projectStatus';
import { ProjectProperty } from '@/typeorm/Entities/ProjectProperty';
import { ProjectStatus as ProjectStatusEntity } from '@/typeorm/Entities/ProjectStatus';
import { parentPort } from 'worker_threads';

/**
Expand All @@ -42,13 +45,41 @@ import { parentPort } from 'worker_threads';
* @returns An object containing the found parcels and buildings that match the search criteria.
*/
const propertiesFuzzySearch = async (keyword: string, limit?: number, agencyIds?: number[]) => {
const allStatusIds = (await AppDataSource.getRepository(ProjectStatusEntity).find()).map(
(i) => i.Id,
);
const allowedStatusIds = [
ProjectStatus.CANCELLED,
ProjectStatus.DENIED,
ProjectStatus.TRANSFERRED_WITHIN_GRE,
];
const disallowedStatusIds = allStatusIds.filter((s) => !allowedStatusIds.includes(s));

// Find all properties that are attached to projects in states other than Cancelled, Transferred within GRE, or Denied
// Get project properties that are in projects currently in the disallowed statuses
const excludedIds = await AppDataSource.getRepository(ProjectProperty).find({
relations: {
Project: true,
},
where: {
Project: {
StatusId: In(disallowedStatusIds),
},
},
});

const excludedParcelIds = excludedIds.map((row) => row.ParcelId).filter((id) => id != null);

const excludedBuildingIds = excludedIds.map((row) => row.BuildingId).filter((id) => id != null);

const parcelsQuery = await AppDataSource.getRepository(Parcel)
.createQueryBuilder('parcel')
.leftJoinAndSelect('parcel.Agency', 'agency')
.leftJoinAndSelect('parcel.AdministrativeArea', 'adminArea')
.leftJoinAndSelect('parcel.Evaluations', 'evaluations')
.leftJoinAndSelect('parcel.Fiscals', 'fiscals')
.leftJoinAndSelect('parcel.Classification', 'classification')
// Match the search criteria
.where(
new Brackets((qb) => {
qb.where(`LPAD(parcel.pid::text, 9, '0') ILIKE '%${keyword.replaceAll('-', '')}%'`)
Expand All @@ -58,7 +89,10 @@ const propertiesFuzzySearch = async (keyword: string, limit?: number, agencyIds?
.orWhere(`parcel.address1 ILIKE '%${keyword}%'`);
}),
)
.andWhere(`classification.Name in ('Surplus Encumbered', 'Surplus Active')`);
// Only include surplus properties
.andWhere(`classification.Name in ('Surplus Encumbered', 'Surplus Active')`)
// Exclude if already is a project property in a project that's in a disallowed status
.andWhere(`parcel.id NOT IN(:...excludedParcelIds)`, { excludedParcelIds });

// Add the optional agencyIds filter if provided
if (agencyIds && agencyIds.length > 0) {
Expand All @@ -76,16 +110,20 @@ const propertiesFuzzySearch = async (keyword: string, limit?: number, agencyIds?
.leftJoinAndSelect('building.Evaluations', 'evaluations')
.leftJoinAndSelect('building.Fiscals', 'fiscals')
.leftJoinAndSelect('building.Classification', 'classification')
// Match the search criteria
.where(
new Brackets((qb) => {
qb.where(`building.pid::text like :keyword`, { keyword: `%${keyword}%` })
.orWhere(`building.pin::text like :keyword`, { keyword: `%${keyword}%` })
.orWhere(`agency.name like :keyword`, { keyword: `%${keyword}%` })
.orWhere(`adminArea.name like :keyword`, { keyword: `%${keyword}%` })
.orWhere(`building.address1 like :keyword`, { keyword: `%${keyword}%` });
qb.where(`LPAD(building.pid::text, 9, '0') ILIKE '%${keyword.replaceAll('-', '')}%'`)
.orWhere(`building.pin::text ILIKE '%${keyword}%'`)
.orWhere(`agency.name ILIKE '%${keyword}%'`)
.orWhere(`adminArea.name ILIKE '%${keyword}%'`)
.orWhere(`building.address1 ILIKE '%${keyword}%'`);
}),
)
.andWhere(`classification.Name in ('Surplus Encumbered', 'Surplus Active')`);
// Only include surplus properties
.andWhere(`classification.Name in ('Surplus Encumbered', 'Surplus Active')`)
// Exclude if already is a project property in a project that's in a disallowed status
.andWhere(`building.id NOT IN(:...excludedBuildingIds)`, { excludedBuildingIds });

if (agencyIds && agencyIds.length > 0) {
buildingsQuery.andWhere(`building.agency_id IN (:...agencyIds)`, { agencyIds });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { ImportResult } from '@/typeorm/Entities/ImportResult';
import { Parcel } from '@/typeorm/Entities/Parcel';
import { ParcelEvaluation } from '@/typeorm/Entities/ParcelEvaluation';
import { ParcelFiscal } from '@/typeorm/Entities/ParcelFiscal';
import { ProjectProperty } from '@/typeorm/Entities/ProjectProperty';
import { ProjectStatus } from '@/typeorm/Entities/ProjectStatus';
import { PropertyClassification } from '@/typeorm/Entities/PropertyClassification';
import { User } from '@/typeorm/Entities/User';
import { MapProperties } from '@/typeorm/Entities/views/MapPropertiesView';
Expand All @@ -38,6 +40,8 @@ import {
produceBuildingEvaluation,
produceBuildingFiscal,
produceSSO,
produceProjectStatus,
produceProjectProperty,
} from 'tests/testUtils/factories';
import { DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
import xlsx, { WorkSheet } from 'xlsx';
Expand Down Expand Up @@ -82,6 +86,46 @@ const _propertyUnionCreateQueryBuilder: any = {
getManyAndCount: () => [[producePropertyUnion()], 1],
};

const _projectStatusCreateQueryBuilder: any = {
select: () => _projectStatusCreateQueryBuilder,
leftJoinAndSelect: () => _projectStatusCreateQueryBuilder,
where: () => _projectStatusCreateQueryBuilder,
orWhere: () => _projectStatusCreateQueryBuilder,
andWhere: () => _projectStatusCreateQueryBuilder,
take: () => _projectStatusCreateQueryBuilder,
skip: () => _projectStatusCreateQueryBuilder,
orderBy: () => _projectStatusCreateQueryBuilder,
getMany: () => [produceProjectStatus()],
};

const _projectPropertyCreateQueryBuilder: any = {
select: () => _projectPropertyCreateQueryBuilder,
leftJoinAndSelect: () => _projectPropertyCreateQueryBuilder,
where: () => _projectPropertyCreateQueryBuilder,
orWhere: () => _projectPropertyCreateQueryBuilder,
andWhere: () => _projectPropertyCreateQueryBuilder,
take: () => _projectPropertyCreateQueryBuilder,
skip: () => _projectPropertyCreateQueryBuilder,
orderBy: () => _projectPropertyCreateQueryBuilder,
getMany: () => [produceProjectProperty()],
};

jest
.spyOn(AppDataSource.getRepository(ProjectProperty), 'createQueryBuilder')
.mockImplementation(() => _projectPropertyCreateQueryBuilder);

jest
.spyOn(AppDataSource.getRepository(ProjectProperty), 'find')
.mockImplementation(async () => [produceProjectProperty()]);

jest
.spyOn(AppDataSource.getRepository(ProjectStatus), 'createQueryBuilder')
.mockImplementation(() => _projectStatusCreateQueryBuilder);

jest
.spyOn(AppDataSource.getRepository(ProjectStatus), 'find')
.mockImplementation(async () => [produceProjectStatus()]);

jest
.spyOn(AppDataSource.getRepository(Parcel), 'createQueryBuilder')
.mockImplementation(() => _parcelsCreateQueryBuilder);
Expand Down
Loading