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-1869 BCA XREF #2734

Merged
merged 7 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
];
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
Loading