Skip to content

Commit cf85f64

Browse files
authored
Merge pull request #477 from bcgov/feature/display-peach-onhold-reasons
feat: display peach on hold reasons and update applicant request reason mapping
2 parents 0b4ec47 + b886847 commit cf85f64

33 files changed

Lines changed: 1174 additions & 400 deletions

app/peachSync.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,36 @@ import { refreshCodeCaches } from './src/db/codes/cache.ts';
44
import { getLogger } from './src/utils/log.ts';
55
import { state } from './state.ts';
66

7-
import type { Permit } from './src/types';
7+
import type { UpdatedPermitWithNote } from './src/types';
88

99
const log = getLogger(module.filename);
1010

1111
async function syncPeachToPcns() {
1212
if (!state.features.peach) return;
1313

1414
const started = Date.now();
15-
let updatedPermits: Permit[];
15+
let updatedPermitsWithNotes: UpdatedPermitWithNote[];
1616

1717
log.info('PEACH sync job started');
1818
try {
19-
updatedPermits = await syncPeachRecords();
19+
updatedPermitsWithNotes = await syncPeachRecords();
2020

2121
log.info('PEACH sync completed', {
2222
durationMs: Date.now() - started,
23-
updatedCount: updatedPermits.length
23+
updatedCount: updatedPermitsWithNotes.length
2424
});
2525
} catch (error) {
2626
log.error('PEACH sync FAILED during data sync', error);
2727
process.exitCode = 1;
2828
return;
2929
}
3030

31-
if (updatedPermits.length === 0) return;
31+
if (updatedPermitsWithNotes.length === 0) return;
3232

3333
try {
34-
for (const permit of updatedPermits) {
35-
await sendPermitUpdateNotifications(permit, true);
34+
for (const permitWithNote of updatedPermitsWithNotes) {
35+
const { permit, note } = permitWithNote;
36+
await sendPermitUpdateNotifications(permit, true, note);
3637
}
3738
} catch (error) {
3839
log.warn('PEACH sync completed but sending notifications failed', error);

app/src/controllers/generalProject.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ const generateGeneralProjectData = async (
147147
statusLastVerifiedTime: null,
148148
issuedPermitId: null,
149149
state: PermitState.IN_PROGRESS,
150+
onHoldCode: null,
150151
submittedDate: x.submittedDate,
151152
submittedTime: x.submittedTime,
152153
decisionDate: null,
@@ -176,6 +177,7 @@ const generateGeneralProjectData = async (
176177
statusLastVerifiedTime: null,
177178
issuedPermitId: null,
178179
state: PermitState.NONE,
180+
onHoldCode: null,
179181
submittedDate: null,
180182
submittedTime: x.submittedTime,
181183
decisionDate: null,

app/src/controllers/housingProject.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ const generateHousingProjectData = async (
205205
statusLastVerifiedTime: null,
206206
issuedPermitId: null,
207207
state: PermitState.IN_PROGRESS,
208+
onHoldCode: null,
208209
submittedDate: x.submittedDate,
209210
submittedTime: x.submittedTime,
210211
decisionDate: null,
@@ -234,6 +235,7 @@ const generateHousingProjectData = async (
234235
statusLastVerifiedTime: null,
235236
issuedPermitId: null,
236237
state: PermitState.NONE,
238+
onHoldCode: null,
237239
submittedDate: null,
238240
submittedTime: x.submittedTime,
239241
decisionDate: null,

app/src/controllers/peach.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PermitStage, PermitState } from '../db/codes/enums.ts';
1+
import { PermitStage, PermitState, PiesOnHold } from '../db/codes/enums.ts';
22
import { transactionWrapper } from '../db/utils/transactionWrapper.ts';
33
import { generateUpdateStamps } from '../db/utils/utils.ts';
44
import { parsePeachRecords, summarizePeachRecord } from '../parsers/peach.ts';
@@ -10,7 +10,13 @@ import { PeachIntegratedSystem } from '../utils/enums/permit.ts';
1010

1111
import type { Request, Response } from 'express';
1212
import type { PrismaTransactionClient } from '../db/dataConnection.ts';
13-
import type { PeachSummary, Permit, Record as PeachRecord, PermitTracking } from '../types/index.ts';
13+
import type {
14+
PeachSummary,
15+
Permit,
16+
Record as PeachRecord,
17+
PermitTracking,
18+
UpdatedPermitWithNote
19+
} from '../types/index.ts';
1420

1521
const log = getLogger(module.filename);
1622

@@ -78,7 +84,7 @@ export const getPeachSummaryController = async (req: Request<never, never, Permi
7884
* Syncs PEACH data for permit tracking to PCNS, returns a list of permits used for sending update notifications
7985
* @returns Array of updated permits
8086
*/
81-
export const syncPeachRecords = async (): Promise<Permit[]> => {
87+
export const syncPeachRecords = async (): Promise<UpdatedPermitWithNote[]> => {
8288
const systemRecordPermits: { recordId: string; systemId: string; permit: Permit }[] = [];
8389
await transactionWrapper<void>(async (tx: PrismaTransactionClient) => {
8490
// Only fetch permits that have a peach integrated system permit type
@@ -110,7 +116,7 @@ export const syncPeachRecords = async (): Promise<Permit[]> => {
110116
}
111117
});
112118

113-
// TODO: May need rate limiting in the future
119+
// Note: May need rate limiting in the future
114120
const results = await Promise.allSettled(
115121
systemRecordPermits.map((srp) => getPeachRecord(srp.recordId, srp.systemId))
116122
);
@@ -137,7 +143,7 @@ export const syncPeachRecords = async (): Promise<Permit[]> => {
137143

138144
const parsedRecords: Record<string, PeachSummary> = parsePeachRecords(records);
139145

140-
const updatedPermits: Permit[] = [];
146+
const updatedPermits: UpdatedPermitWithNote[] = [];
141147

142148
await transactionWrapper<void>(async (tx: PrismaTransactionClient) => {
143149
for (const systemRecordPermit of systemRecordPermits) {
@@ -167,14 +173,21 @@ export const syncPeachRecords = async (): Promise<Permit[]> => {
167173
const lastChangedDatesEqual = compareDates(peachStatusChangedDatetime, pcnsStatusChangedDatetime) === 0;
168174
const stagesEqual = peachSummary.stage === (pcnsPermit.stage as PermitStage);
169175
const statesEqual = peachSummary.state === (pcnsPermit.state as PermitState);
176+
const onHoldCodesEqual = peachSummary.onHoldCode === pcnsPermit.onHoldCode;
170177

178+
const stageOrStateHasDiff = !stagesEqual || !statesEqual;
171179
const hasDiff =
172-
!stagesEqual || !statesEqual || !submittedDatesEqual || !decisionDatesEqual || !lastChangedDatesEqual;
180+
stageOrStateHasDiff ||
181+
!onHoldCodesEqual ||
182+
!submittedDatesEqual ||
183+
!decisionDatesEqual ||
184+
!lastChangedDatesEqual;
173185

174186
if (!hasDiff) continue;
175187

176188
pcnsPermit.stage = peachSummary.stage;
177189
pcnsPermit.state = peachSummary.state;
190+
pcnsPermit.onHoldCode = peachSummary.onHoldCode;
178191

179192
pcnsPermit.submittedDate = peachSummary.submittedDate;
180193
pcnsPermit.submittedTime = peachSummary.submittedTime;
@@ -198,11 +211,16 @@ export const syncPeachRecords = async (): Promise<Permit[]> => {
198211
pcnsPermit.updatedBy = updatedBy;
199212

200213
const cleanedPermit = omit(pcnsPermit, ['permitTracking', 'permitType']);
201-
202214
const updatedPermit = await upsertPermit(tx, cleanedPermit);
215+
const applicantRequestedHold = !onHoldCodesEqual && peachSummary.onHoldCode === PiesOnHold.APPLICANT_REQUEST;
216+
let note: string | undefined = undefined;
217+
218+
if (applicantRequestedHold) {
219+
note = 'This application has been placed on hold at the applicant’s request';
220+
}
203221

204-
// Only return permits and notes that have had a status change for notifications
205-
if (!stagesEqual || !statesEqual) updatedPermits.push(updatedPermit);
222+
// For notifications, only return permits that have had a status change or is now on hold by applicant's request.
223+
if (stageOrStateHasDiff || applicantRequestedHold) updatedPermits.push({ permit: updatedPermit, note });
206224
}
207225
});
208226

app/src/db/codes/enums.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,12 @@ export const PermitState = {
136136
} as const;
137137

138138
export type PermitState = (typeof PermitState)[keyof typeof PermitState];
139+
140+
export const PiesOnHold = {
141+
APPLICANT_REQUEST: 'APPLICANT_REQUEST',
142+
LEGAL_ACTION: 'LEGAL_ACTION',
143+
MISSING_INFORMATION: 'MISSING_INFORMATION',
144+
PENDING_EXTERNAL_DECISION: 'PENDING_EXTERNAL_DECISION'
145+
} as const;
146+
147+
export type PiesOnHold = (typeof PiesOnHold)[keyof typeof PiesOnHold];
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import stamps from '../stamps.ts';
2+
import {
3+
createAuditLogTrigger,
4+
createUpdatedAtTrigger,
5+
dropAuditLogTrigger,
6+
dropUpdatedAtTrigger
7+
} from '../utils/utils.ts';
8+
9+
import type { Knex } from 'knex';
10+
11+
export async function up(knex: Knex): Promise<void> {
12+
return (
13+
Promise.resolve()
14+
// Create code tables
15+
.then(() =>
16+
knex.schema.createTable('pies_on_hold_code', (table) => {
17+
// Constrains to SCREAMING_SNAKE w/ no double or trailing underscores
18+
table.text('code').primary().checkRegex('^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$');
19+
table.text('display').unique().notNullable();
20+
table.text('definition').notNullable();
21+
table.boolean('active').notNullable().defaultTo(true);
22+
stamps(knex, table);
23+
})
24+
)
25+
26+
// Create before update & audit triggers
27+
.then(async () => await createUpdatedAtTrigger(knex, 'public', 'pies_on_hold_code'))
28+
.then(async () => await createAuditLogTrigger(knex, 'public', 'pies_on_hold_code'))
29+
30+
// Seeding code table
31+
.then(() => {
32+
const onHoldCodes = [
33+
{
34+
code: 'APPLICANT_REQUEST',
35+
display: 'Applicant Request',
36+
definition: 'The application is paused as requested by applicant and supported by staff.'
37+
},
38+
{
39+
code: 'LEGAL_ACTION',
40+
display: 'Legal Action',
41+
definition:
42+
'The application is subject to legal proceedings, or legal counsel has advised that processing be paused.'
43+
},
44+
{
45+
code: 'MISSING_INFORMATION',
46+
display: 'Missing Information',
47+
definition: 'More information is required from the applicant to proceed with the review.'
48+
},
49+
{
50+
code: 'PENDING_EXTERNAL_DECISION',
51+
display: 'Pending External Decision',
52+
definition:
53+
'The application requires decision, approval, or input from another agency or an external process.'
54+
}
55+
];
56+
57+
return knex('pies_on_hold_code').insert(onHoldCodes);
58+
})
59+
60+
// Add on_hold_code to permit table
61+
.then(() =>
62+
knex.schema.alterTable('permit', function (table) {
63+
table
64+
.text('on_hold_code')
65+
.references('code')
66+
.inTable('pies_on_hold_code')
67+
.onUpdate('CASCADE')
68+
.onDelete('SET NULL');
69+
})
70+
)
71+
72+
// Set on_hold_code to MISSING_INFORMATION for PEACH integrated permits in 'Pending client action' state
73+
.then(() => {
74+
const peachIntegratedPermitTypes = [32, 34, 35, 8, 9, 11, 12, 10, 7, 31, 21, 19, 20, 22, 23];
75+
76+
return knex('permit')
77+
.where('state', 'PENDING_APPLICANT_ACTION')
78+
.whereIn('permit_type_id', peachIntegratedPermitTypes)
79+
.update({
80+
on_hold_code: 'MISSING_INFORMATION'
81+
});
82+
})
83+
);
84+
}
85+
86+
export async function down(knex: Knex): Promise<void> {
87+
return (
88+
Promise.resolve()
89+
// Drop on_hold_code column
90+
.then(() =>
91+
knex.schema.alterTable('permit', function (table) {
92+
table.dropColumn('on_hold_code');
93+
})
94+
)
95+
96+
// Drop triggers
97+
.then(async () => await dropUpdatedAtTrigger(knex, 'public', 'pies_on_hold_code'))
98+
.then(async () => await dropAuditLogTrigger(knex, 'public', 'pies_on_hold_code'))
99+
100+
// Drop code table
101+
.then(() => knex.schema.dropTableIfExists('pies_on_hold_code'))
102+
);
103+
}

0 commit comments

Comments
 (0)