Skip to content

Commit

Permalink
Enquiry form, submission, drafts & note creation for relations
Browse files Browse the repository at this point in the history
  • Loading branch information
kyle1morel committed May 21, 2024
1 parent 9a10337 commit 545b0be
Show file tree
Hide file tree
Showing 24 changed files with 924 additions and 25 deletions.
3 changes: 2 additions & 1 deletion app/src/components/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ export const PERMIT_STATUS = Object.freeze({
/** Types of notes */
export const NOTE_TYPE_LIST = Object.freeze({
GENERAL: 'General',
BRING_FORWARD: 'Bring Forward'
BRING_FORWARD: 'Bring Forward',
ENQUIRY: 'Enquiry'
});

/**
Expand Down
124 changes: 124 additions & 0 deletions app/src/controllers/enquiry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { NIL, v4 as uuidv4 } from 'uuid';

import { Initiatives, NOTE_TYPE_LIST } from '../components/constants';
import { getCurrentIdentity } from '../components/utils';
import { activityService, enquiryService, noteService, userService } from '../services';

import type { NextFunction, Request, Response } from '../interfaces/IExpress';
import type { Enquiry } from '../types';

const controller = {
generateEnquiryData: async (req: Request) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data: any = req.body;

const activityId = data.activityId ?? (await activityService.createActivity(Initiatives.HOUSING))?.activityId;

let applicant, basic;

// Create applicant information
if (data.applicant) {
applicant = {
contactFirstName: data.applicant.firstName,
contactLastName: data.applicant.lastName,
contactPhoneNumber: data.applicant.phoneNumber,
contactEmail: data.applicant.email,
contactApplicantRelationship: data.applicant.relationshipToProject,
contactPreference: data.applicant.contactPreference
};
}

if (data.basic) {
basic = {
enquiryType: data.basic.enquiryType,
isRelated: data.basic.isRelated,
relatedActivityId: data.basic.relatedActivityId,
enquiryDescription: data.basic.enquiryDescription,
applyForPermitConnect: data.basic.applyForPermitConnect
};
}

// Put new enquiry together
return {
...applicant,
...basic,
enquiryId: data.enquiryId ?? uuidv4(),
activityId: activityId,
submittedAt: data.submittedAt ?? new Date().toISOString(),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
submittedBy: (req.currentUser?.tokenPayload as any)?.idir_username
};
},

createDraft: async (req: Request, res: Response, next: NextFunction) => {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data: any = req.body;

const enquiry = await controller.generateEnquiryData(req);

// Create new enquiry
const result = await enquiryService.createEnquiry(enquiry);

// On submit attempt to create note if enquiry is associated with an existing activity
if (data.submit && result.relatedActivityId) {
const activity = await activityService.getActivity(result.relatedActivityId);
if (activity) {
const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, NIL), NIL);
await noteService.createNote({
activityId: result.relatedActivityId,
note: `Added by ${(req.currentUser?.tokenPayload as any)?.idir_username}\nEnquiry #${result.activityId}\n${result.enquiryDescription}`,
noteType: NOTE_TYPE_LIST.ENQUIRY,
title: 'Enquiry',
bringForwardDate: null,
bringForwardState: null,
createdAt: new Date().toISOString(),
createdBy: userId,
isDeleted: false
});
}
}

res.status(201).json({ activityId: result.activityId, enquiryId: result.enquiryId });
} catch (e: unknown) {
next(e);
}
},

updateDraft: async (req: Request, res: Response, next: NextFunction) => {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data: any = req.body;

const enquiry = await controller.generateEnquiryData(req);

// Update enquiry
const result = await enquiryService.updateEnquiry(enquiry as Enquiry);

// On submit, attempt to create note if enquiry is associated with an existing activity
if (data.submit && result.relatedActivityId) {
const activity = await activityService.getActivity(result.relatedActivityId);
if (activity) {
const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, NIL), NIL);
await noteService.createNote({
activityId: result.relatedActivityId,
note: `Added by ${(req.currentUser?.tokenPayload as any)?.idir_username}\nEnquiry #${result.activityId}`,
noteType: NOTE_TYPE_LIST.ENQUIRY,
title: 'Enquiry',
bringForwardDate: null,
bringForwardState: null,
createdAt: new Date().toISOString(),
createdBy: userId,
isDeleted: false
});
}
}

res.status(200).json({ activityId: result.activityId, enquiryId: result.enquiryId });
} catch (e: unknown) {
next(e);
}
}
};

export default controller;
1 change: 1 addition & 0 deletions app/src/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { default as documentController } from './document';
export { default as enquiryController } from './enquiry';
export { default as noteController } from './note';
export { default as permitController } from './permit';
export { default as roadmapController } from './roadmap';
Expand Down
87 changes: 87 additions & 0 deletions app/src/db/migrations/20240516000000_005-shas-enquiry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import stamps from '../stamps';

import type { Knex } from 'knex';

/*
* Not included in this migration is a manual destructive operation
* submission.contact_name will be split into two new parts
* submission.contact_first_name
* submission.contact_last_name
* submission.contact_name will then be dropped
*/

export async function up(knex: Knex): Promise<void> {
return (
Promise.resolve()
// Create public schema tables
.then(() =>
knex.schema.createTable('enquiry', (table) => {
table.uuid('enquiry_id').primary();
table
.text('activity_id')
.notNullable()
.references('activity_id')
.inTable('activity')
.onUpdate('CASCADE')
.onDelete('CASCADE');
table.uuid('assigned_user_id').references('user_id').inTable('user').onUpdate('CASCADE').onDelete('CASCADE');
table.timestamp('submitted_at', { useTz: true }).notNullable();
table.text('submitted_by').notNullable();
table.text('contact_first_name');
table.text('contact_last_name');
table.text('contact_phone_number');
table.text('contact_email');
table.text('contact_preference');
table.text('contact_applicant_relationship');
table.text('is_related');
table.text('related_activity_id');
table.text('enquiry_description');
table.text('apply_for_permit_connect');
stamps(knex, table);
})
)

.then(() =>
knex.schema.alterTable('submission', (table) => {
table.text('contact_first_name');
table.text('contact_last_name');
})
)

// Create public schema table triggers
.then(() =>
knex.schema.raw(`create trigger before_update_enquiry_trigger
before update on "enquiry"
for each row execute procedure public.set_updated_at();`)
)

// Create audit triggers
.then(() =>
knex.schema.raw(`CREATE TRIGGER audit_enquiry_trigger
AFTER UPDATE OR DELETE ON enquiry
FOR EACH ROW EXECUTE PROCEDURE audit.if_modified_func();`)
)
);
}

export async function down(knex: Knex): Promise<void> {
return (
Promise.resolve()
// Drop audit triggers
.then(() => knex.schema.raw('DROP TRIGGER IF EXISTS audit_enquiry_trigger ON permit'))

// Drop public schema table triggers
.then(() => knex.schema.raw('DROP TRIGGER IF EXISTS before_update_enquiry_trigger ON "enquiry"'))

// Revert table alters
.then(() =>
knex.schema.alterTable('submission', function (table) {
table.dropColumn('contact_first_name');
table.dropColumn('contact_last_name');
})
)

// Drop public schema tables
.then(() => knex.schema.dropTableIfExists('enquiry'))
);
}
70 changes: 70 additions & 0 deletions app/src/db/models/enquiry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Prisma } from '@prisma/client';

import user from './user';

import type { Stamps } from '../stamps';
import type { Enquiry } from '../../types';

// Define types
const _enquiry = Prisma.validator<Prisma.enquiryDefaultArgs>()({});
const _enquiryWithGraph = Prisma.validator<Prisma.enquiryDefaultArgs>()({});
const _enquiryWithUserGraph = Prisma.validator<Prisma.enquiryDefaultArgs>()({ include: { user: true } });

type PrismaRelationEnquiry = Omit<Prisma.enquiryGetPayload<typeof _enquiry>, keyof Stamps>;
type PrismaGraphEnquiry = Prisma.enquiryGetPayload<typeof _enquiryWithGraph>;
type PrismaGraphEnquiryUser = Prisma.enquiryGetPayload<typeof _enquiryWithUserGraph>;

export default {
toPrismaModel(input: Enquiry): PrismaRelationEnquiry {
return {
enquiry_id: input.enquiryId,
activity_id: input.activityId,
assigned_user_id: input.assignedUserId,
submitted_at: new Date(input.submittedAt ?? Date.now()),
submitted_by: input.submittedBy,
contact_first_name: input.contactFirstName,
contact_last_name: input.contactLastName,
contact_phone_number: input.contactPhoneNumber,
contact_email: input.contactEmail,
contact_preference: input.contactPreference,
contact_applicant_relationship: input.contactApplicantRelationship,
enquiry_type: input.enquiryType,
is_related: input.isRelated,
related_activity_id: input.relatedActivityId,
enquiry_description: input.enquiryDescription,
apply_for_permit_connect: input.applyForPermitConnect
};
},

fromPrismaModel(input: PrismaGraphEnquiry): Enquiry {
return {
enquiryId: input.enquiry_id,
activityId: input.activity_id,
assignedUserId: input.assigned_user_id,
submittedAt: input.submitted_at?.toISOString() as string,
submittedBy: input.submitted_by,
contactName: input.contact_name,
contactPhoneNumber: input.contact_phone_number,
contactEmail: input.contact_email,
contactPreference: input.contact_preference,
contactApplicantRelationship: input.contact_applicant_relationship,
enquiryType: input.enquiry_type,
isRelated: input.is_related,
relatedActivityId: input.related_activity_id,
enquiryDescription: input.enquiry_description,
applyForPermitConnect: input.apply_for_permit_connect,
user: null
};
},

fromPrismaModelWithUser(input: PrismaGraphEnquiryUser | null): Enquiry | null {
if (!input) return null;

const enquiry = this.fromPrismaModel(input);
if (enquiry && input.user) {
enquiry.user = user.fromPrismaModel(input.user);
}

return enquiry;
}
};
1 change: 1 addition & 0 deletions app/src/db/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { default as activity } from './activity';
export { default as document } from './document';
export { default as enquiry } from './enquiry';
export { default as identity_provider } from './identity_provider';
export { default as note } from './note';
export { default as permit } from './permit';
Expand Down
28 changes: 28 additions & 0 deletions app/src/db/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ model activity {
updated_at DateTime? @db.Timestamptz(6)
initiative initiative @relation(fields: [initiative_id], references: [initiative_id], onDelete: Cascade, map: "activity_initiative_id_foreign")
document document[]
enquiry enquiry[]
note note[]
permit permit[]
submission submission[]
Expand Down Expand Up @@ -190,6 +191,8 @@ model submission {
indigenous_description String?
non_profit_description String?
housing_coop_description String?
contact_first_name String?
contact_last_name String?
activity activity @relation(fields: [activity_id], references: [activity_id], onDelete: Cascade, map: "submission_activity_id_foreign")
user user? @relation(fields: [assigned_user_id], references: [user_id], onDelete: Cascade, map: "submission_assigned_user_id_foreign")
}
Expand All @@ -208,6 +211,7 @@ model user {
created_at DateTime? @default(now()) @db.Timestamptz(6)
updated_by String?
updated_at DateTime? @db.Timestamptz(6)
enquiry enquiry[]
submission submission[]
identity_provider identity_provider? @relation(fields: [idp], references: [idp], onDelete: Cascade, map: "user_idp_foreign")
Expand All @@ -216,6 +220,30 @@ model user {
@@index([username], map: "user_username_index")
}

model enquiry {
enquiry_id String @id @db.Uuid
activity_id String
assigned_user_id String? @db.Uuid
submitted_at DateTime @db.Timestamptz(6)
submitted_by String
contact_first_name String?
contact_last_name String?
contact_phone_number String?
contact_email String?
contact_preference String?
contact_applicant_relationship String?
is_related String?
related_activity_id String?
enquiry_description String?
apply_for_permit_connect String?
created_by String? @default("00000000-0000-0000-0000-000000000000")
created_at DateTime? @default(now()) @db.Timestamptz(6)
updated_by String?
updated_at DateTime? @db.Timestamptz(6)
activity activity @relation(fields: [activity_id], references: [activity_id], onDelete: Cascade, map: "enquiry_activity_id_foreign")
user user? @relation(fields: [assigned_user_id], references: [user_id], onDelete: Cascade, map: "enquiry_assigned_user_id_foreign")
}

enum yes_no_unsure {
Yes
No
Expand Down
20 changes: 20 additions & 0 deletions app/src/routes/v1/enquiry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import express from 'express';
import { enquiryController } from '../../controllers';
import { requireSomeAuth } from '../../middleware/requireSomeAuth';

import type { NextFunction, Request, Response } from '../../interfaces/IExpress';

const router = express.Router();
router.use(requireSomeAuth);

// Submission create draft endpoint
router.put('/draft', (req: Request, res: Response, next: NextFunction): void => {
enquiryController.createDraft(req, res, next);
});

// Submission update draft endpoint
router.put('/draft/:activityId', (req: Request, res: Response, next: NextFunction): void => {
enquiryController.updateDraft(req, res, next);
});

export default router;
Loading

0 comments on commit 545b0be

Please sign in to comment.