Skip to content

Commit 921ad8e

Browse files
authored
Merge pull request #326 from bcgov/feature/intake-contact-selection
Contact Selection in Nav Project/Enquiry Forms
2 parents 39cfbeb + 07011da commit 921ad8e

22 files changed

Lines changed: 917 additions & 168 deletions

File tree

app/src/controllers/contact.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@ const controller = {
5454
}
5555
},
5656

57+
matchContacts: async (
58+
req: Request<never, never, ContactSearchParameters, never>,
59+
res: Response,
60+
next: NextFunction
61+
) => {
62+
try {
63+
const response = await contactService.matchContacts(req.body);
64+
res.status(200).json(response);
65+
} catch (e: unknown) {
66+
next(e);
67+
}
68+
},
69+
5770
searchContacts: async (
5871
req: Request<never, never, never, ContactSearchParameters>,
5972
res: Response,

app/src/controllers/electrificationProject.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { v4 as uuidv4 } from 'uuid';
22

33
import { generateCreateStamps, generateUpdateStamps } from '../db/utils/utils';
44
import {
5+
activityContactService,
56
activityService,
67
contactService,
78
draftService,
@@ -10,7 +11,7 @@ import {
1011
} from '../services';
1112
import { Initiative } from '../utils/enums/application';
1213
import { ApplicationStatus, DraftCode, IntakeStatus, SubmissionType } from '../utils/enums/projectCommon';
13-
import { isTruthy } from '../utils/utils';
14+
import { partition, isTruthy } from '../utils/utils';
1415

1516
import type { NextFunction, Request, Response } from 'express';
1617
import type {
@@ -315,12 +316,31 @@ const controller = {
315316
next: NextFunction
316317
) => {
317318
try {
318-
// If Navigator created empty electrification project we need to assign contactIds on save
319-
req.body.contacts = req.body.contacts.map((x) => {
320-
if (!x.contactId) x.contactId = uuidv4();
321-
return x;
322-
});
323-
await contactService.upsertContacts(req.body.contacts, req.currentContext, req.body.project.activityId);
319+
if (req.body.contacts) {
320+
// Predicate function to check if a contact has a contactId.
321+
// Used to partition contacts into existing (with contactId) and new (without contactId).
322+
const hasContactId = (x: Contact) => !!x.contactId;
323+
324+
// Partition contacts into existing and new based on whether they have a contactId
325+
const [existingContacts, newContacts] = partition(req.body.contacts, hasContactId);
326+
327+
// Assign a new contactId to each new contact
328+
newContacts.forEach((x) => {
329+
x.contactId = uuidv4();
330+
});
331+
332+
// Combine existing contacts with new contacts
333+
const contacts = existingContacts.concat(newContacts);
334+
335+
// Insert new contacts into the contact table
336+
await contactService.insertContacts(newContacts, req.currentContext);
337+
338+
// Delete any activity_contact records that doesn't match the activity and contacts in the request
339+
await activityContactService.deleteUnmatchedActivityContacts(req.body.project.activityId, contacts);
340+
341+
// Create or update activity_contact with the data from the request
342+
await activityContactService.upsertActivityContacts(req.body.project.activityId, contacts);
343+
}
324344

325345
const response = await electrificationProjectService.updateElectrificationProject({
326346
...req.body.project,

app/src/controllers/enquiry.ts

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import { NIL, v4 as uuidv4 } from 'uuid';
22

33
import { generateCreateStamps, generateUpdateStamps } from '../db/utils/utils';
4-
import { activityService, contactService, enquiryService, noteService, userService } from '../services';
4+
import {
5+
activityService,
6+
activityContactService,
7+
contactService,
8+
enquiryService,
9+
noteService,
10+
userService
11+
} from '../services';
512
import { Initiative } from '../utils/enums/application';
613
import { ApplicationStatus, IntakeStatus, NoteType, SubmissionType } from '../utils/enums/projectCommon';
7-
import { getCurrentSubject, getCurrentUsername, isTruthy } from '../utils/utils';
14+
import { getCurrentSubject, getCurrentUsername, partition, isTruthy } from '../utils/utils';
815

916
import type { NextFunction, Request, Response } from 'express';
10-
import type { Enquiry, EnquiryIntake, EnquirySearchParameters } from '../types';
17+
import type { Contact, Enquiry, EnquiryIntake, EnquirySearchParameters } from '../types';
1118

1219
const controller = {
1320
/**
@@ -177,14 +184,31 @@ const controller = {
177184

178185
updateEnquiry: async (req: Request<never, never, Enquiry>, res: Response, next: NextFunction) => {
179186
try {
180-
// Assign contactId if not present
181187
if (req.body.contacts) {
182-
req.body.contacts = req.body.contacts.map((x) => ({
183-
...x,
184-
contactId: x.contactId ?? uuidv4()
185-
}));
188+
// Predicate function to check if a contact has a contactId.
189+
// Used to partition contacts into existing (with contactId) and new (without contactId).
190+
const hasContactId = (x: Contact) => !!x.contactId;
191+
192+
// Partition contacts into exisiting and new based on whether they have a contactId
193+
const [existingContacts, newContacts] = partition(req.body.contacts, hasContactId);
194+
195+
// Assign a new contactId to each new contact
196+
newContacts.forEach((x) => {
197+
x.contactId = uuidv4();
198+
});
199+
200+
// Combine existing contacts with new contacts
201+
const contacts = existingContacts.concat(newContacts);
202+
203+
// Insert new contacts into the contact table
204+
await contactService.insertContacts(newContacts, req.currentContext);
205+
206+
// Delete any activity_contact records that doesn't match the activity and contacts in the request
207+
await activityContactService.deleteUnmatchedActivityContacts(req.body.activityId, contacts);
208+
209+
// Create or update activity_contact with the data from the request
210+
await activityContactService.upsertActivityContacts(req.body.activityId, contacts);
186211
}
187-
await contactService.upsertContacts(req.body.contacts, req.currentContext, req.body.activityId);
188212

189213
const result = await enquiryService.updateEnquiry({
190214
...req.body,

app/src/controllers/housingProject.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { v4 as uuidv4 } from 'uuid';
22

33
import { generateCreateStamps, generateUpdateStamps } from '../db/utils/utils';
44
import {
5+
activityContactService,
56
activityService,
67
contactService,
78
draftService,
@@ -14,7 +15,7 @@ import { Initiative } from '../utils/enums/application';
1415
import { NumResidentialUnits } from '../utils/enums/housing';
1516
import { PermitAuthorizationStatus, PermitNeeded, PermitStatus } from '../utils/enums/permit';
1617
import { ApplicationStatus, DraftCode, IntakeStatus, SubmissionType } from '../utils/enums/projectCommon';
17-
import { getCurrentUsername, isTruthy } from '../utils/utils';
18+
import { getCurrentUsername, partition, isTruthy } from '../utils/utils';
1819

1920
import type { NextFunction, Request, Response } from 'express';
2021
import type {
@@ -465,13 +466,31 @@ const controller = {
465466
next: NextFunction
466467
) => {
467468
try {
468-
// If Navigator created empty housing project we need to assign contactIds on save
469-
const contacts = req.body.contacts?.map((x) => {
470-
if (!x.contactId) x.contactId = uuidv4();
471-
return x;
472-
});
469+
if (req.body.contacts) {
470+
// Predicate function to check if a contact has a contactId.
471+
// Used to partition contacts into existing (with contactId) and new (without contactId).
472+
const hasContactId = (x: Contact) => !!x.contactId;
473+
474+
// Partition contacts into existing and new based on whether they have a contactId
475+
const [existingContacts, newContacts] = partition(req.body.contacts, hasContactId);
476+
477+
// Assign a new contactId to each new contact
478+
newContacts.forEach((x) => {
479+
x.contactId = uuidv4();
480+
});
473481

474-
if (contacts) await contactService.upsertContacts(contacts, req.currentContext, req.body.activityId);
482+
// Combine existing contacts with new contacts
483+
const contacts = existingContacts.concat(newContacts);
484+
485+
// Insert new contacts into the contact table
486+
await contactService.insertContacts(newContacts, req.currentContext);
487+
488+
// Delete any activity_contact records that doesn't match the activity and contacts in the request
489+
await activityContactService.deleteUnmatchedActivityContacts(req.body.activityId, contacts);
490+
491+
// Create or update activity_contact with the data from the request
492+
await activityContactService.upsertActivityContacts(req.body.activityId, contacts);
493+
}
475494

476495
const response = await housingProjectService.updateHousingProject({
477496
...req.body,

app/src/docs/v1.api-spec.yaml

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,12 @@ paths:
140140
delete:
141141
summary: Delete a contact
142142
description: >-
143-
Deletes the contact with the supplied **`contactId`**.
144-
Returns **204 No Content** when the record is deleted.
143+
Deletes the contact with the supplied **`contactId`**.
144+
Returns **204 No Content** when the record is deleted.
145145
Returns **404 Not Found** when no matching contact exists.
146146
tags:
147147
- Contacts
148-
operationId: deleteContact
148+
operationId: deleteContact
149149
parameters:
150150
- $ref: '#/components/parameters/Path-ContactId'
151151
responses:
@@ -159,6 +159,53 @@ paths:
159159
$ref: '#/components/responses/NotFound'
160160
default:
161161
$ref: '#/components/responses/Error'
162+
/contact/match:
163+
post:
164+
summary: Match contacts
165+
description: Returns a list of contacts if any of the given queries match
166+
operationId: matchContacts
167+
tags:
168+
- Contact
169+
requestBody:
170+
required: true
171+
content:
172+
application/json:
173+
schema:
174+
type: object
175+
properties:
176+
email:
177+
type: string
178+
description: Match by specific email address
179+
example: JaneDoe@email.com
180+
firstName:
181+
type: string
182+
description: Match by specific first name
183+
example: Jane
184+
lastName:
185+
type: string
186+
description: Match by specific last name
187+
example: Doe
188+
phoneNumber:
189+
type: string
190+
description: Match by specific phone number
191+
example: (123) 456-7890
192+
responses:
193+
'200':
194+
description: A list of contacts matching the search criteria
195+
content:
196+
application/json:
197+
schema:
198+
type: array
199+
items:
200+
$ref: '#/components/schemas/DB-Contact'
201+
"401":
202+
$ref: "#/components/responses/Unauthorized"
203+
'403':
204+
$ref: '#/components/responses/Forbidden'
205+
"422":
206+
$ref: "#/components/responses/UnprocessableEntity"
207+
default:
208+
$ref: '#/components/responses/Error'
162209
/contact/search:
163210
get:
164211
summary: Search contacts

app/src/routes/v1/contact.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ router.get(
2323
}
2424
);
2525

26+
/** Match contacts endpoint */
27+
router.post(
28+
'/match',
29+
hasAuthorization(Resource.CONTACT, Action.READ),
30+
contactValidator.matchContacts,
31+
(req: Request<never, never, ContactSearchParameters, never>, res: Response, next: NextFunction): void => {
32+
contactController.matchContacts(req, res, next);
33+
}
34+
);
35+
2636
/** Search contacts endpoint */
2737
router.get(
2838
'/search',
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import prisma from '../db/dataConnection';
2+
3+
import type { Contact } from '../types';
4+
import activity_contact from '../db/models/activity_contact';
5+
6+
const service = {
7+
/**
8+
* Deletes activity_contact records that do not match the provided activityId and contacts
9+
* @param activityId
10+
* @param contacts
11+
*/
12+
13+
deleteUnmatchedActivityContacts: async (activityId: string, contacts: Array<Contact>) => {
14+
const requestContactIds = contacts.map((c) => c.contactId);
15+
await prisma.activity_contact.deleteMany({
16+
where: {
17+
activity_id: activityId,
18+
contact_id: {
19+
notIn: requestContactIds
20+
}
21+
}
22+
});
23+
},
24+
25+
/**
26+
* Upserts activity_contact records for the given activityId and contacts
27+
* @param activityId
28+
* @param contacts
29+
*/
30+
upsertActivityContacts: async (activityId: string, contacts: Array<Contact>) => {
31+
return await prisma.$transaction(async (trx) => {
32+
await Promise.all(
33+
contacts.map(async (x: Contact) => {
34+
await trx.activity_contact.upsert({
35+
where: {
36+
activity_id_contact_id: {
37+
activity_id: activityId,
38+
contact_id: x.contactId
39+
}
40+
},
41+
update: {
42+
...activity_contact.toPrismaModel({
43+
activityId: activityId,
44+
contactId: x.contactId
45+
})
46+
},
47+
create: {
48+
...activity_contact.toPrismaModel({
49+
activityId: activityId,
50+
contactId: x.contactId
51+
})
52+
}
53+
});
54+
})
55+
);
56+
});
57+
}
58+
};
59+
60+
export default service;

0 commit comments

Comments
 (0)