Skip to content

Commit

Permalink
PIMS-1869 BCA XREF (#2734)
Browse files Browse the repository at this point in the history
Co-authored-by: LawrenceLau2020 <68400651+LawrenceLau2020@users.noreply.github.com>
  • Loading branch information
dbarkowsky and LawrenceLau2020 authored Oct 23, 2024
1 parent e95d9b8 commit 2aad8e2
Show file tree
Hide file tree
Showing 11 changed files with 201 additions and 14 deletions.
23 changes: 23 additions & 0 deletions express-api/src/controllers/tools/toolsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Request, Response } from 'express';
import chesServices from '@/services/ches/chesServices';
import { ChesFilterSchema } from './toolsSchema';
import geocoderService from '@/services/geocoder/geocoderService';
import { AppDataSource } from '@/appDataSource';
import { JurRollPidXref } from '@/typeorm/Entities/JurRollPidXref';

/**
* NOTE
Expand Down Expand Up @@ -90,3 +92,24 @@ export const searchGeocoderAddresses = async (req: Request, res: Response) => {
const geoReturn = await geocoderService.getSiteAddresses(address, minScore, maxResults);
return res.status(200).send(geoReturn);
};

/**
* Retrieves jurisdiction & roll number based on PID.
* Used to cross reference BC Assessment records with Parcels.
* @param req - The request object.
* @param res - The response object.
* @returns A response with the jurisdiction, roll number, and PID if found.
*/
export const getJurisdictionRollNumberByPid = async (req: Request, res: Response) => {
const pidQuery = req.query.pid as string;
if (parseInt(pidQuery)) {
const result = await AppDataSource.getRepository(JurRollPidXref).findOne({
where: { PID: parseInt(pidQuery) },
});
if (!result) {
return res.status(404).send('PID not found.');
}
return res.status(200).send(result);
}
return res.status(400).send('Invalid PID value.');
};
2 changes: 1 addition & 1 deletion express-api/src/express.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ app.use(`/v2/buildings`, protectedRoute(), userAuthCheck(), router.buildingsRout
app.use(`/v2/notifications`, protectedRoute(), router.notificationsRouter);
app.use(`/v2/projects`, protectedRoute(), router.projectsRouter);
app.use(`/v2/reports`, protectedRoute(), userAuthCheck(), router.reportsRouter);
app.use(`/v2/tools`, protectedRoute(), userAuthCheck(), router.toolsRouter);
app.use(`/v2/tools`, protectedRoute(), router.toolsRouter);

// If a non-existent route is called. Must go after other routes.
app.use('*', (_req, _res, next) => next(EndpointNotFound404));
Expand Down
53 changes: 50 additions & 3 deletions express-api/src/routes/tools.swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@ paths:
This response comes from the BC Geocoder Service.
Capable of any error code from BC Geocoder.
parameters:
- in: path
- in: query
name: address
schema:
type: string
example: 742 Evergreen Terr
- in: path
- in: query
name: minScore
schema:
type: integer
example: 30
- in: path
- in: query
name: maxResults
schema:
type: integer
Expand All @@ -40,9 +40,56 @@ paths:
schema:
type: string
example: Failed to fetch data
/tools/jur-roll-xref:
get:
security:
- bearerAuth: []
tags:
- Tools
summary: Returns a record matching the provided PID.
description: >
Used to cross reference the PID and Jurisdiction Code + Roll Number from BC Assessment.
parameters:
- in: query
name: pid
schema:
type: integer
example: 111222333
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/JurRollXref'
'400':
content:
text/plain:
schema:
type: string
example: Invalid PID value.
'404':
content:
text/plain:
schema:
type: string
example: PID not found.
### SCHEMAS ###
components:
schemas:
JurRollXref:
type: object
properties:
PID:
type: integer
example: 123456789
JurisdictionCode:
type: string
example: '123'
RollNumber:
type: string
example: '1342341'
GeocoderAddress:
type: object
properties:
Expand Down
11 changes: 10 additions & 1 deletion express-api/src/routes/toolsRouter.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { Roles } from '@/constants/roles';
import controllers from '@/controllers';
import { getJurisdictionRollNumberByPid } from '@/controllers/tools/toolsController';
import userAuthCheck from '@/middleware/userAuthCheck';
import catchErrors from '@/utilities/controllerErrorWrapper';
import express from 'express';

const router = express.Router();

const { searchGeocoderAddresses } = controllers;

router.route(`/geocoder/addresses`).get(catchErrors(searchGeocoderAddresses));
router.route(`/geocoder/addresses`).get(userAuthCheck(), catchErrors(searchGeocoderAddresses));
router
.route(`/jur-roll-xref`)
.get(
userAuthCheck({ requiredRoles: [Roles.ADMIN] }),
catchErrors(getJurisdictionRollNumberByPid),
);

export default router;
16 changes: 16 additions & 0 deletions express-api/src/typeorm/Entities/JurRollPidXref.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Entity, PrimaryColumn } from 'typeorm';

/**
* Used to cross reference the records from BC Assessment and find one that matches a PID.
*/
@Entity()
export class JurRollPidXref {
@PrimaryColumn({ type: 'int', name: 'pid' })
PID: number;

@PrimaryColumn({ type: 'character varying', length: 3, name: 'jurisdiction_code' })
JurisdictionCode: string;

@PrimaryColumn({ type: 'character varying', length: 15, name: 'roll_number' })
RollNumber: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class CreateXrefTable1729627184522 implements MigrationInterface {
name = 'CreateXrefTable1729627184522';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "jur_roll_pid_xref" ("pid" integer NOT NULL, "jurisdiction_code" character varying(3) NOT NULL, "roll_number" character varying(15) NOT NULL, CONSTRAINT "PK_68f4d54ea088bb438e6100af993" PRIMARY KEY ("pid", "jurisdiction_code", "roll_number"))`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "jur_roll_pid_xref"`);
}
}
2 changes: 2 additions & 0 deletions express-api/src/typeorm/entitiesIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { ImportResult } from './Entities/ImportResult';
import { ProjectJoin } from './Entities/views/ProjectJoinView';
import { AdministrativeAreaJoinView } from '@/typeorm/Entities/views/AdministrativeAreaJoinView';
import { AgencyJoinView } from '@/typeorm/Entities/views/AgencyJoinView';
import { JurRollPidXref } from '@/typeorm/Entities/JurRollPidXref';

const views = [
BuildingRelations,
Expand Down Expand Up @@ -97,5 +98,6 @@ export default [
User,
NoteType,
ImportResult,
JurRollPidXref,
...views,
];
36 changes: 36 additions & 0 deletions express-api/tests/unit/controllers/tools/toolsController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
produceEmailStatus,
} from '../../../testUtils/factories';
import { randomUUID } from 'crypto';
import { AppDataSource } from '@/appDataSource';
import { JurRollPidXref } from '@/typeorm/Entities/JurRollPidXref';

const _getChesMessageStatusById = jest.fn().mockImplementation(() => produceEmailStatus({}));

Expand Down Expand Up @@ -34,6 +36,14 @@ jest.mock('@/services/ches/chesServices.ts', () => ({
sendEmailAsync: () => _sendEmailAsync(),
}));

const _xrefSpy = jest
.spyOn(AppDataSource.getRepository(JurRollPidXref), 'findOne')
.mockImplementation(async () => ({
JurisdictionCode: '123',
RollNumber: '1234567',
PID: 111222333,
}));

describe('UNIT - Tools', () => {
let mockRequest: Request & MockReq, mockResponse: Response & MockRes;

Expand Down Expand Up @@ -163,4 +173,30 @@ describe('UNIT - Tools', () => {
expect(mockResponse.statusValue).toBe(200);
});
});

describe('GET /tools/jur-roll-xref', () => {
it('should return 200 if given a valid PID', async () => {
mockRequest.query.pid = '2134';
await controllers.getJurisdictionRollNumberByPid(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
expect(mockResponse.sendValue).toHaveProperty('PID');
expect(mockResponse.sendValue).toHaveProperty('JurisdictionCode');
expect(mockResponse.sendValue).toHaveProperty('RollNumber');
});

it('should return 400 if given an invalid PID', async () => {
mockRequest.query.pid = 'hi';
await controllers.getJurisdictionRollNumberByPid(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(400);
expect(mockResponse.sendValue).toBe('Invalid PID value.');
});

it('should return 404 if a record with that PID is not found', async () => {
mockRequest.query.pid = '1234';
_xrefSpy.mockImplementationOnce(async () => null);
await controllers.getJurisdictionRollNumberByPid(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(404);
expect(mockResponse.sendValue).toBe('PID not found.');
});
});
});
40 changes: 33 additions & 7 deletions react-app/src/components/map/parcelPopup/ParcelPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ParcelData } from '@/hooks/api/useParcelLayerApi';
import usePimsApi from '@/hooks/usePimsApi';
import { Box, Grid, IconButton, Typography, Tab, SxProps } from '@mui/material';
import { LatLng } from 'leaflet';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { Popup, useMap, useMapEvents } from 'react-leaflet';
import KeyboardDoubleArrowLeftIcon from '@mui/icons-material/KeyboardDoubleArrowLeft';
import KeyboardDoubleArrowRightIcon from '@mui/icons-material/KeyboardDoubleArrowRight';
Expand All @@ -16,6 +16,7 @@ import BCAssessmentDetails from '@/components/map/parcelPopup/BCAssessmentDetail
import LtsaDetails from '@/components/map/parcelPopup/LtsaDetails';
import ParcelLayerDetails from '@/components/map/parcelPopup/ParcelLayerDeatils';
import ParcelPopupSelect from '@/components/map/parcelPopup/ParcelPopupSelect';
import { JurRollPidXref } from '@/hooks/api/useBCAssessmentApi';

interface ParcelPopupProps {
size?: 'small' | 'large';
Expand Down Expand Up @@ -55,6 +56,14 @@ export const ParcelPopup = (props: ParcelPopupProps) => {
api.bcAssessment.getBCAssessmentByLocation(clickPosition.lng, clickPosition.lat),
);

const {
data: xrefData,
refreshData: refreshXref,
isLoading: xrefLoading,
} = useDataLoader(() =>
api.bcAssessment.getJurisdictionRoleByPid(parcelData.at(parcelIndex)?.PID_NUMBER),
);

const map = useMap();
const api = usePimsApi();

Expand All @@ -67,6 +76,7 @@ export const ParcelPopup = (props: ParcelPopupProps) => {
if (parcelData && clickPosition) {
refreshLtsa();
if (pimsUser.hasOneOfRoles([Roles.ADMIN])) {
refreshXref();
refreshBCA();
}
}
Expand Down Expand Up @@ -106,6 +116,26 @@ export const ParcelPopup = (props: ParcelPopupProps) => {
}
}, [clickPosition]);

/**
* Gets a matching record from BC Assessment data by using the XREF table in the database to
* match the PID with a corresponding Jurisdiction Code and Roll Number.
*/
const getMatchingBcaRecord = useMemo(() => {
if (!xrefData || !bcAssessmentData) {
return undefined;
}
if (xrefData.status !== 200) {
return undefined;
}
const xrefRecord = xrefData.parsedBody as JurRollPidXref;
const matchingFolio = bcAssessmentData.features.find(
(folio) =>
folio.properties.JURISDICTION_CODE === xrefRecord.JurisdictionCode &&
folio.properties.ROLL_NUMBER === xrefRecord.RollNumber,
);
return matchingFolio;
}, [bcAssessmentData, xrefData]);

if (!clickPosition) return <></>;

const tabPanelStyle: SxProps = {
Expand Down Expand Up @@ -162,12 +192,8 @@ export const ParcelPopup = (props: ParcelPopupProps) => {
</TabPanel>
<TabPanel value="2" sx={tabPanelStyle}>
<BCAssessmentDetails
data={
bcAssessmentData && bcAssessmentData.features.length
? bcAssessmentData.features.at(0).properties
: undefined
}
isLoading={bcaLoading}
data={getMatchingBcaRecord ? getMatchingBcaRecord?.properties : undefined}
isLoading={bcaLoading || xrefLoading}
width={POPUP_WIDTH}
/>
</TabPanel>
Expand Down
15 changes: 14 additions & 1 deletion react-app/src/hooks/api/useBCAssessmentApi.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { IFetch } from '@/hooks/useFetch';
import { BBox, FeatureCollection, Geometry } from 'geojson';

export interface BCAssessmentProperties {
Expand Down Expand Up @@ -29,7 +30,13 @@ export interface BCAssessmentProperties {
bbox: BBox;
}

const useBCAssessmentApi = () => {
export interface JurRollPidXref {
PID: number;
JurisdictionCode: string;
RollNumber: string;
}

const useBCAssessmentApi = (absoluteFetch: IFetch) => {
const url = window.location.href.includes('pims.gov.bc.ca')
? 'https://apps.gov.bc.ca/ext/sgw/geo.bca?REQUEST=GetFeature&SERVICE=WFS&VERSION=2.0.0&typeName=geo.bca:WHSE_HUMAN_CULTURAL_ECONOMIC.BCA_FOLIO_GNRL_PROP_VALUES_SV&outputFormat=application/json'
: 'https://test.apps.gov.bc.ca/ext/sgw/geo.bca?REQUEST=GetFeature&SERVICE=WFS&VERSION=2.0.0&typeName=geo.bca:WHSE_HUMAN_CULTURAL_ECONOMIC.BCA_FOLIO_GNRL_PROP_VALUES_SV&outputFormat=application/json';
Expand All @@ -44,8 +51,14 @@ const useBCAssessmentApi = () => {
return body as FeatureCollection<Geometry, BCAssessmentProperties>;
};

const getJurisdictionRoleByPid = async (pid: number) => {
const response = await absoluteFetch.get(`/tools/jur-roll-xref?pid=${pid}`);
return response;
};

return {
getBCAssessmentByLocation,
getJurisdictionRoleByPid,
};
};

Expand Down
2 changes: 1 addition & 1 deletion react-app/src/hooks/usePimsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const usePimsApi = () => {
const tools = useToolsApi(fetch);
const parcelLayer = useParcelLayerApi(fetch);
const projects = useProjectsApi(fetch);
const bcAssessment = useBCAssessmentApi();
const bcAssessment = useBCAssessmentApi(fetch);
const ltsa = useLtsaApi(fetch);
const notifications = useProjectNotificationsApi(fetch);

Expand Down

0 comments on commit 2aad8e2

Please sign in to comment.