From 13294e55b714c39404a46480c55c2f76432adb62 Mon Sep 17 00:00:00 2001 From: Kyle Morel Date: Wed, 15 May 2024 13:32:09 -0700 Subject: [PATCH 1/3] Allow saving and updating of intake drafts --- app/src/controllers/submission.ts | 266 ++++++----- app/src/db/models/permit.ts | 4 +- app/src/routes/v1/submission.ts | 10 + app/src/services/activity.ts | 26 ++ app/src/services/permit.ts | 20 +- app/src/services/submission.ts | 19 +- app/tests/unit/controllers/submission.spec.ts | 429 ++++++++++++++++-- .../form/internal/InputTextInternal.vue | 4 +- .../src/components/intake/ShasIntakeForm.vue | 55 ++- frontend/src/services/submissionService.ts | 16 + 10 files changed, 681 insertions(+), 168 deletions(-) diff --git a/app/src/controllers/submission.ts b/app/src/controllers/submission.ts index bb6c2dc1..083dd7bd 100644 --- a/app/src/controllers/submission.ts +++ b/app/src/controllers/submission.ts @@ -4,14 +4,14 @@ import { NIL, v4 as uuidv4 } from 'uuid'; import { APPLICATION_STATUS_LIST, INTAKE_STATUS_LIST, + Initiatives, PERMIT_NEEDED, PERMIT_STATUS, YesNo, YesNoUnsure } from '../components/constants'; import { camelCaseToTitleCase, deDupeUnsure, getCurrentIdentity, toTitleCase } from '../components/utils'; -import { generateUniqueActivityId } from '../db/utils/utils'; -import { submissionService, permitService, userService } from '../services'; +import { activityService, submissionService, permitService, userService } from '../services'; import type { NextFunction, Request, Response } from '../interfaces/IExpress'; import type { ChefsFormConfig, ChefsFormConfigData, Submission, ChefsSubmissionExport, Permit } from '../types'; @@ -167,114 +167,130 @@ const controller = { notStored.map((x) => x.permits?.map(async (y) => await permitService.createPermit(y))); }, - createSubmission: async (req: Request, res: Response, next: NextFunction) => { - try { - const newActivityId = await generateUniqueActivityId(); + generateSubmissionData: async (req: Request, intakeStatus: string) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const data: any = req.body; - // 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, housing, location, permits; + let appliedPermits: Array = [], + investigatePermits: Array = []; + + // Create applicant information + if (data.applicant) { + applicant = { + contactName: `${data.applicant.firstName} ${data.applicant.lastName}`, + contactPhoneNumber: data.applicant.phoneNumber, + contactEmail: data.applicant.email, + contactApplicantRelationship: data.applicant.relationshipToProject, + contactPreference: data.applicant.contactPreference + }; + } - let applicant, basic, housing, location, permits; - let appliedPermits: Array = [], - investigatePermits: Array = []; - - // Create applicant information - if (data.applicant) { - applicant = { - contactName: `${data.applicant.firstName} ${data.applicant.lastName}`, - contactPhoneNumber: data.applicant.phoneNumber, - contactEmail: data.applicant.email, - contactApplicantRelationship: data.applicant.relationshipToProject, - contactPreference: data.applicant.contactPreference - }; - } - - if (data.basic) { - basic = { - isDevelopedByCompanyOrOrg: data.basic.isDevelopedByCompanyOrOrg, - isDevelopedInBC: data.basic.isDevelopedInBC, - companyNameRegistered: data.basic.registeredName - }; - } - - if (data.housing) { - housing = { - projectName: data.housing.projectName, - projectDescription: data.housing.projectDescription, - //singleFamilySelected: true, // not necessary to save - check if singleFamilyUnits not null - //multiFamilySelected: true, // not necessary to save - check if multiFamilyUnits not null - singleFamilyUnits: data.housing.singleFamilyUnits, - multiFamilyUnits: data.housing.multiFamilyUnits, - //otherSelected: true, // not necessary to save - check if otherUnits not null - otherUnitsDescription: data.housing.otherUnitsDescription, - otherUnits: data.housing.otherUnits, - hasRentalUnits: data.housing.hasRentalUnits, - financiallySupportedBC: data.housing.financiallySupportedBC, - financiallySupportedIndigenous: data.housing.financiallySupportedIndigenous, - financiallySupportedNonProfit: data.housing.financiallySupportedNonProfit, - financiallySupportedHousingCoop: data.housing.financiallySupportedHousingCoop, - rentalUnits: data.housing.rentalUnits, - indigenousDescription: data.housing.indigenousDescription, - nonProfitDescription: data.housing.nonProfitDescription, - housingCoopDescription: data.housing.housingCoopDescription - }; - } - - if (data.location) { - location = { - naturalDisaster: data.location.naturalDisaster, - projectLocation: data.location.projectLocation, - locationPIDs: data.location.ltsaPIDLookup, - latitude: data.location.latitude, - longitude: data.location.longitude, - //addressSearch: 'Search address', // not necessary to save - client side search field - streetAddress: data.location.streetAddress, - locality: data.location.locality, - province: data.location.province - }; - } - - if (data.permits) { - permits = { - hasAppliedProvincialPermits: data.permits.hasAppliedProvincialPermits, - checkProvincialPermits: data.permits.checkProvincialPermits - }; - } - - if (data.appliedPermits && data.appliedPermits.length) { - appliedPermits = data.appliedPermits.map((x: Permit) => ({ - permitTypeId: x.permitTypeId, - activityId: newActivityId, - trackingId: x.trackingId, - status: PERMIT_STATUS.APPLIED, - statusLastVerified: x.statusLastVerified - })); - } - - if (data.investigatePermits && data.investigatePermits.length) { - investigatePermits = data.investigatePermits.flatMap((x: Permit) => ({ - permitTypeId: x.permitTypeId, - activityId: newActivityId, - needed: PERMIT_NEEDED.UNDER_INVESTIGATION, - statusLastVerified: x.statusLastVerified - })); - } - - // Put new submission together - const submission = { + if (data.basic) { + basic = { + isDevelopedByCompanyOrOrg: data.basic.isDevelopedByCompanyOrOrg, + isDevelopedInBC: data.basic.isDevelopedInBC, + companyNameRegistered: data.basic.registeredName + }; + } + + if (data.housing) { + housing = { + projectName: data.housing.projectName, + projectDescription: data.housing.projectDescription, + //singleFamilySelected: true, // not necessary to save - check if singleFamilyUnits not null + //multiFamilySelected: true, // not necessary to save - check if multiFamilyUnits not null + singleFamilyUnits: data.housing.singleFamilyUnits, + multiFamilyUnits: data.housing.multiFamilyUnits, + //otherSelected: true, // not necessary to save - check if otherUnits not null + otherUnitsDescription: data.housing.otherUnitsDescription, + otherUnits: data.housing.otherUnits, + hasRentalUnits: data.housing.hasRentalUnits, + financiallySupportedBC: data.housing.financiallySupportedBC, + financiallySupportedIndigenous: data.housing.financiallySupportedIndigenous, + financiallySupportedNonProfit: data.housing.financiallySupportedNonProfit, + financiallySupportedHousingCoop: data.housing.financiallySupportedHousingCoop, + rentalUnits: data.housing.rentalUnits, + indigenousDescription: data.housing.indigenousDescription, + nonProfitDescription: data.housing.nonProfitDescription, + housingCoopDescription: data.housing.housingCoopDescription + }; + } + + if (data.location) { + location = { + naturalDisaster: data.location.naturalDisaster === YesNo.YES, + projectLocation: data.location.projectLocation, + locationPIDs: data.location.ltsaPIDLookup, + latitude: data.location.latitude, + longitude: data.location.longitude, + //addressSearch: 'Search address', // not necessary to save - client side search field + streetAddress: data.location.streetAddress, + locality: data.location.locality, + province: data.location.province + }; + } + + if (data.permits) { + permits = { + hasAppliedProvincialPermits: data.permits.hasAppliedProvincialPermits, + checkProvincialPermits: data.permits.checkProvincialPermits + }; + } + + if (data.appliedPermits && data.appliedPermits.length) { + appliedPermits = data.appliedPermits.map((x: Permit) => ({ + permitId: x.permitId, + permitTypeId: x.permitTypeId, + activityId: activityId, + trackingId: x.trackingId, + status: PERMIT_STATUS.APPLIED, + statusLastVerified: x.statusLastVerified + })); + } + + if (data.investigatePermits && data.investigatePermits.length) { + investigatePermits = data.investigatePermits.flatMap((x: Permit) => ({ + permitId: x.permitId, + permitTypeId: x.permitTypeId, + activityId: activityId, + needed: PERMIT_NEEDED.UNDER_INVESTIGATION, + statusLastVerified: x.statusLastVerified + })); + } + + // Put new submission together + return { + submission: { ...applicant, ...basic, ...housing, ...location, ...permits, - submissionId: uuidv4(), - activityId: newActivityId, - submittedAt: new Date().toISOString(), + submissionId: data.submissionId ?? 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, - intakeStatus: INTAKE_STATUS_LIST.SUBMITTED, - applicationStatus: APPLICATION_STATUS_LIST.NEW - }; + intakeStatus: intakeStatus, + applicationStatus: data.applicationStatus ?? APPLICATION_STATUS_LIST.NEW + }, + appliedPermits, + investigatePermits + }; + }, + + createDraft: async (req: Request, res: Response, next: NextFunction) => { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const data: any = req.body; + + const { submission, appliedPermits, investigatePermits } = await controller.generateSubmissionData( + req, + data.submit ? INTAKE_STATUS_LIST.SUBMITTED : INTAKE_STATUS_LIST.DRAFT + ); // Create new submission const result = await submissionService.createSubmission(submission); @@ -283,7 +299,27 @@ const controller = { await Promise.all(appliedPermits.map(async (x: Permit) => await permitService.createPermit(x))); await Promise.all(investigatePermits.map(async (x: Permit) => await permitService.createPermit(x))); - res.status(201).json({ activityId: result.activityId }); + res.status(201).json({ activityId: result.activityId, submissionId: result.submissionId }); + } catch (e: unknown) { + next(e); + } + }, + + createSubmission: async (req: Request, res: Response, next: NextFunction) => { + try { + const { submission, appliedPermits, investigatePermits } = await controller.generateSubmissionData( + req, + INTAKE_STATUS_LIST.SUBMITTED + ); + + // Create new submission + const result = await submissionService.createSubmission(submission); + + // Create each permit + await Promise.all(appliedPermits.map(async (x: Permit) => await permitService.createPermit(x))); + await Promise.all(investigatePermits.map(async (x: Permit) => await permitService.createPermit(x))); + + res.status(201).json({ activityId: result.activityId, submissionId: result.submissionId }); } catch (e: unknown) { next(e); } @@ -324,6 +360,32 @@ const controller = { } }, + updateDraft: async (req: Request, res: Response, next: NextFunction) => { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const data: any = req.body; + + const { submission, appliedPermits, investigatePermits } = await controller.generateSubmissionData( + req, + data.submit ? INTAKE_STATUS_LIST.SUBMITTED : INTAKE_STATUS_LIST.DRAFT + ); + + // Update submission + const result = await submissionService.updateSubmission(submission as Submission); + + // Remove already existing permits for this activity + await permitService.deletePermitsByActivity(submission.activityId); + + // Create each permit + await Promise.all(appliedPermits.map(async (x: Permit) => await permitService.createPermit(x))); + await Promise.all(investigatePermits.map(async (x: Permit) => await permitService.createPermit(x))); + + res.status(200).json({ activityId: result.activityId, submissionId: result.submissionId }); + } catch (e: unknown) { + next(e); + } + }, + updateSubmission: async (req: Request, res: Response, next: NextFunction) => { try { const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, NIL), NIL); diff --git a/app/src/db/models/permit.ts b/app/src/db/models/permit.ts index 96efd00d..46045932 100644 --- a/app/src/db/models/permit.ts +++ b/app/src/db/models/permit.ts @@ -29,9 +29,7 @@ export default { }; }, - fromPrismaModel(input: PrismaGraphPermit | null): Permit | null { - if (!input) return null; - + fromPrismaModel(input: PrismaGraphPermit): Permit { return { permitId: input.permit_id, permitTypeId: input.permit_type_id, diff --git a/app/src/routes/v1/submission.ts b/app/src/routes/v1/submission.ts index 0f1d011a..ca1bae51 100644 --- a/app/src/routes/v1/submission.ts +++ b/app/src/routes/v1/submission.ts @@ -22,6 +22,16 @@ router.get( } ); +// Submission create draft endpoint +router.put('/draft', (req: Request, res: Response, next: NextFunction): void => { + submissionController.createDraft(req, res, next); +}); + +// Submission update draft endpoint +router.put('/draft/:activityId', (req: Request, res: Response, next: NextFunction): void => { + submissionController.updateDraft(req, res, next); +}); + // Submission create endpoint router.put('/', (req: Request, res: Response, next: NextFunction): void => { submissionController.createSubmission(req, res, next); diff --git a/app/src/services/activity.ts b/app/src/services/activity.ts index 17cb037f..0e265d23 100644 --- a/app/src/services/activity.ts +++ b/app/src/services/activity.ts @@ -1,7 +1,33 @@ import prisma from '../db/dataConnection'; import { activity } from '../db/models'; +import { generateUniqueActivityId } from '../db/utils/utils'; const service = { + /** + * @function createActivity + * Create an activity for the given initiative with a unique identifier + * @param {string} initiative The initiative ID + * @returns {Promise} The result of running the findFirst operation + */ + createActivity: async (initiative: string) => { + const response = await prisma.$transaction(async (trx) => { + const initiativeResult = await trx.initiative.findFirstOrThrow({ + where: { + code: initiative + } + }); + + return await trx.activity.create({ + data: { + activity_id: await generateUniqueActivityId(), + initiative_id: initiativeResult.initiative_id + } + }); + }); + + return activity.fromPrismaModel(response); + }, + /** * @function getActivity * Get an activity diff --git a/app/src/services/permit.ts b/app/src/services/permit.ts index d9a1a725..e6959aef 100644 --- a/app/src/services/permit.ts +++ b/app/src/services/permit.ts @@ -33,7 +33,7 @@ const service = { * @function deletePermit * Delete a permit * @param {string} permitId Permit ID - * @returns {Promise} The result of running the delete operation + * @returns {Promise} The result of running the delete operation */ deletePermit: async (permitId: string) => { const response = await prisma.permit.delete({ @@ -48,6 +48,22 @@ const service = { return permit.fromPrismaModel(response); }, + /** + * @function deletePermitByActivity + * Delete a permit + * @param {string} activityId Activity ID to remove permits from + * @returns {number} The result of running the deleteMany operation + */ + deletePermitsByActivity: async (activityId: string) => { + const response = await prisma.permit.deleteMany({ + where: { + activity_id: activityId + } + }); + + return response.count; + }, + /** * @function getPermitTypes * Get all Permit types @@ -66,7 +82,7 @@ const service = { * @function listPermits * Retrieve a list of permits associated with a given activity * @param {string} activityId PCNS Activity ID - * @returns {Promise<(Permit | null)[]>} The result of running the findMany operation + * @returns {Promise} The result of running the findMany operation */ listPermits: async (activityId: string) => { const response = await prisma.permit.findMany({ diff --git a/app/src/services/submission.ts b/app/src/services/submission.ts index e00aae0f..0225609f 100644 --- a/app/src/services/submission.ts +++ b/app/src/services/submission.ts @@ -32,23 +32,8 @@ const service = { * @returns {Promise>} The result of running the transaction */ createSubmission: async (data: Partial) => { - const response = await prisma.$transaction(async (trx) => { - const initiative = await trx.initiative.findFirstOrThrow({ - where: { - code: Initiatives.HOUSING - } - }); - - await trx.activity.create({ - data: { - activity_id: data.activityId as string, - initiative_id: initiative.initiative_id - } - }); - - return await trx.submission.create({ - data: submission.toPrismaModel(data as Submission) - }); + const response = await prisma.submission.create({ + data: submission.toPrismaModel(data as Submission) }); return submission.fromPrismaModel(response); diff --git a/app/tests/unit/controllers/submission.spec.ts b/app/tests/unit/controllers/submission.spec.ts index 0f8bb872..e0e656f5 100644 --- a/app/tests/unit/controllers/submission.spec.ts +++ b/app/tests/unit/controllers/submission.spec.ts @@ -1,7 +1,7 @@ import config from 'config'; import { NIL } from 'uuid'; -import { APPLICATION_STATUS_LIST, INTAKE_STATUS_LIST } from '../../../src/components/constants'; +import { APPLICATION_STATUS_LIST, INTAKE_STATUS_LIST, Initiatives } from '../../../src/components/constants'; import submissionController from '../../../src/controllers/submission'; import { activityService, permitService, submissionService, userService } from '../../../src/services'; import * as utils from '../../../src/components/utils'; @@ -290,7 +290,6 @@ describe('checkAndStoreNewSubmissions', () => { formExportSpy.mockResolvedValueOnce([FORM_EXPORT_1]).mockResolvedValueOnce([]); searchSubmissionsSpy.mockResolvedValue([]); createSubmissionsFromExportSpy.mockResolvedValue(); - createPermitSpy.mockResolvedValue(null); await submissionController.checkAndStoreNewSubmissions(); @@ -318,7 +317,7 @@ describe('checkAndStoreNewSubmissions', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any searchSubmissionsSpy.mockResolvedValue([SUBMISSION_1 as any]); createSubmissionsFromExportSpy.mockResolvedValue(); - createPermitSpy.mockResolvedValue(null); + createPermitSpy.mockResolvedValue({} as Permit); await submissionController.checkAndStoreNewSubmissions(); @@ -359,7 +358,7 @@ describe('checkAndStoreNewSubmissions', () => { formExportSpy.mockResolvedValueOnce([FORM_EXPORT_2]).mockResolvedValueOnce([]); searchSubmissionsSpy.mockResolvedValue([]); createSubmissionsFromExportSpy.mockResolvedValue(); - createPermitSpy.mockResolvedValue(null); + createPermitSpy.mockResolvedValue({} as Permit); await submissionController.checkAndStoreNewSubmissions(); @@ -376,63 +375,202 @@ describe('checkAndStoreNewSubmissions', () => { }); }); -describe('createSubmission', () => { +describe('createDraft', () => { // Mock service calls const createPermitSpy = jest.spyOn(permitService, 'createPermit'); const createSubmissionSpy = jest.spyOn(submissionService, 'createSubmission'); - const getActivitySpy = jest.spyOn(activityService, 'getActivity'); + const createActivitySpy = jest.spyOn(activityService, 'createActivity'); - it('should return 201 and new activity ID if all good', async () => { + it('creates submission with unique activity ID', async () => { const req = { - body: {}, + body: { ...SUBMISSION_1, activityId: undefined, submissionId: undefined }, currentUser: CURRENT_USER }; const next = jest.fn(); - getActivitySpy.mockResolvedValue(null); - createSubmissionSpy.mockResolvedValue({ activityId: '00000000' } as Submission); + createActivitySpy.mockResolvedValue({ activityId: '00000000', initiativeId: Initiatives.HOUSING }); + createSubmissionSpy.mockResolvedValue({ activityId: '00000000', submissionId: '11111111' } as Submission); // eslint-disable-next-line @typescript-eslint/no-explicit-any - await submissionController.createSubmission(req as any, res as any, next); + await submissionController.createDraft(req as any, res as any, next); - expect(getActivitySpy).toHaveBeenCalledTimes(1); + expect(createActivitySpy).toHaveBeenCalledTimes(1); expect(createSubmissionSpy).toHaveBeenCalledTimes(1); expect(res.status).toHaveBeenCalledWith(201); - expect(res.json).toHaveBeenCalledWith({ activityId: '00000000' }); + expect(res.json).toHaveBeenCalledWith({ activityId: '00000000', submissionId: '11111111' }); }); - it('creates submission with unique activity ID', async () => { + it('populates data from body if it exists', async () => { + const isoPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; + const req = { - body: SUBMISSION_1, + body: { + applicant: { + firstName: 'Test', + lastName: 'User' + }, + basic: { + isDevelopedByCompanyOrOrg: true + }, + housing: { + projectName: 'TheProject' + }, + location: { + projectLocation: 'Some place' + }, + permits: { + hasAppliedProvincialPermits: true + } + }, currentUser: CURRENT_USER }; const next = jest.fn(); - getActivitySpy.mockResolvedValue(null); + createActivitySpy.mockResolvedValue({ activityId: '00000000', initiativeId: Initiatives.HOUSING }); createSubmissionSpy.mockResolvedValue({ activityId: '00000000' } as Submission); // eslint-disable-next-line @typescript-eslint/no-explicit-any - await submissionController.createSubmission(req as any, res as any, next); + await submissionController.createDraft(req as any, res as any, next); - expect(getActivitySpy).toHaveBeenCalledTimes(1); + expect(createActivitySpy).toHaveBeenCalledTimes(1); expect(createSubmissionSpy).toHaveBeenCalledTimes(1); + expect(createSubmissionSpy).toHaveBeenCalledWith( + expect.objectContaining({ + contactName: `${req.body.applicant.firstName} ${req.body.applicant.lastName}`, + isDevelopedByCompanyOrOrg: true, + projectName: 'TheProject', + projectLocation: 'Some place', + hasAppliedProvincialPermits: true, + submissionId: expect.any(String), + activityId: '00000000', + submittedAt: expect.stringMatching(isoPattern), + intakeStatus: INTAKE_STATUS_LIST.DRAFT, + applicationStatus: APPLICATION_STATUS_LIST.NEW + }) + ); }); - it('attemps to create unique activity ID again on conflict', async () => { + it('sets intake status to Submitted when submit flag given', async () => { const req = { - body: SUBMISSION_1, + body: { + activityId: '00000000', + submissionId: '11111111', + submit: true + }, currentUser: CURRENT_USER }; const next = jest.fn(); - getActivitySpy.mockResolvedValueOnce({ activityId: '00000000', initiativeId: '123' }).mockResolvedValueOnce(null); createSubmissionSpy.mockResolvedValue({ activityId: '00000000' } as Submission); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await submissionController.createDraft(req as any, res as any, next); + + expect(createActivitySpy).toHaveBeenCalledTimes(0); + expect(createSubmissionSpy).toHaveBeenCalledTimes(1); + expect(createSubmissionSpy).toHaveBeenCalledWith( + expect.objectContaining({ + intakeStatus: INTAKE_STATUS_LIST.SUBMITTED + }) + ); + }); + + it('creates permits if they exist', async () => { + const now = new Date().toISOString(); + + const req = { + body: { + appliedPermits: [ + { + permitTypeId: 1, + trackingId: '123', + status: 'Applied', + statusLastVerified: now + }, + { + permitTypeId: 3, + trackingId: '456', + status: 'Applied', + statusLastVerified: now + } + ], + investigatePermits: [ + { + permitTypeId: 12, + needed: 'Under investigation', + statusLastVerified: now + } + ] + }, + currentUser: CURRENT_USER + }; + const next = jest.fn(); + + createActivitySpy.mockResolvedValue({ activityId: '00000000', initiativeId: Initiatives.HOUSING }); + createSubmissionSpy.mockResolvedValue({ activityId: '00000000' } as Submission); + createPermitSpy.mockResolvedValue({} as Permit); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await submissionController.createDraft(req as any, res as any, next); + + expect(createActivitySpy).toHaveBeenCalledTimes(1); + expect(createSubmissionSpy).toHaveBeenCalledTimes(1); + + expect(createPermitSpy).toHaveBeenCalledTimes(3); + expect(createPermitSpy).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + permitTypeId: 1, + activityId: '00000000', + trackingId: '123', + status: 'Applied', + statusLastVerified: now + }) + ); + expect(createPermitSpy).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + permitTypeId: 3, + activityId: '00000000', + trackingId: '456', + status: 'Applied', + statusLastVerified: now + }) + ); + expect(createPermitSpy).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ + permitTypeId: 12, + activityId: '00000000', + needed: 'Under investigation', + statusLastVerified: now + }) + ); + }); +}); + +describe('createSubmission', () => { + // Mock service calls + const createPermitSpy = jest.spyOn(permitService, 'createPermit'); + const createSubmissionSpy = jest.spyOn(submissionService, 'createSubmission'); + const createActivitySpy = jest.spyOn(activityService, 'createActivity'); + + it('creates submission with unique activity ID', async () => { + const req = { + body: { ...SUBMISSION_1, activityId: undefined, submissionId: undefined }, + currentUser: CURRENT_USER + }; + const next = jest.fn(); + + createActivitySpy.mockResolvedValue({ activityId: '00000000', initiativeId: Initiatives.HOUSING }); + createSubmissionSpy.mockResolvedValue({ activityId: '00000000', submissionId: '11111111' } as Submission); // eslint-disable-next-line @typescript-eslint/no-explicit-any await submissionController.createSubmission(req as any, res as any, next); - expect(getActivitySpy).toHaveBeenCalledTimes(2); + expect(createActivitySpy).toHaveBeenCalledTimes(1); expect(createSubmissionSpy).toHaveBeenCalledTimes(1); + expect(res.status).toHaveBeenCalledWith(201); + expect(res.json).toHaveBeenCalledWith({ activityId: '00000000', submissionId: '11111111' }); }); it('populates data from body if it exists', async () => { @@ -461,13 +599,13 @@ describe('createSubmission', () => { }; const next = jest.fn(); - getActivitySpy.mockResolvedValue(null); + createActivitySpy.mockResolvedValue({ activityId: '00000000', initiativeId: Initiatives.HOUSING }); createSubmissionSpy.mockResolvedValue({ activityId: '00000000' } as Submission); // eslint-disable-next-line @typescript-eslint/no-explicit-any await submissionController.createSubmission(req as any, res as any, next); - expect(getActivitySpy).toHaveBeenCalledTimes(1); + expect(createActivitySpy).toHaveBeenCalledTimes(1); expect(createSubmissionSpy).toHaveBeenCalledTimes(1); expect(createSubmissionSpy).toHaveBeenCalledWith( expect.objectContaining({ @@ -477,7 +615,7 @@ describe('createSubmission', () => { projectLocation: 'Some place', hasAppliedProvincialPermits: true, submissionId: expect.any(String), - activityId: expect.any(String), + activityId: '00000000', submittedAt: expect.stringMatching(isoPattern), intakeStatus: INTAKE_STATUS_LIST.SUBMITTED, applicationStatus: APPLICATION_STATUS_LIST.NEW @@ -516,14 +654,14 @@ describe('createSubmission', () => { }; const next = jest.fn(); - getActivitySpy.mockResolvedValue(null); + createActivitySpy.mockResolvedValue({ activityId: '00000000', initiativeId: Initiatives.HOUSING }); createSubmissionSpy.mockResolvedValue({ activityId: '00000000' } as Submission); - createPermitSpy.mockResolvedValue(null); + createPermitSpy.mockResolvedValue({} as Permit); // eslint-disable-next-line @typescript-eslint/no-explicit-any await submissionController.createSubmission(req as any, res as any, next); - expect(getActivitySpy).toHaveBeenCalledTimes(1); + expect(createActivitySpy).toHaveBeenCalledTimes(1); expect(createSubmissionSpy).toHaveBeenCalledTimes(1); expect(createPermitSpy).toHaveBeenCalledTimes(3); @@ -531,7 +669,7 @@ describe('createSubmission', () => { 1, expect.objectContaining({ permitTypeId: 1, - activityId: expect.any(String), + activityId: '00000000', trackingId: '123', status: 'Applied', statusLastVerified: now @@ -541,7 +679,7 @@ describe('createSubmission', () => { 2, expect.objectContaining({ permitTypeId: 3, - activityId: expect.any(String), + activityId: '00000000', trackingId: '456', status: 'Applied', statusLastVerified: now @@ -551,7 +689,7 @@ describe('createSubmission', () => { 3, expect.objectContaining({ permitTypeId: 12, - activityId: expect.any(String), + activityId: '00000000', needed: 'Under investigation', statusLastVerified: now }) @@ -747,6 +885,235 @@ describe('getSubmissions', () => { }); }); +describe('updateDraft', () => { + // Mock service calls + const createPermitSpy = jest.spyOn(permitService, 'createPermit'); + const updateSubmissionSpy = jest.spyOn(submissionService, 'updateSubmission'); + const createActivitySpy = jest.spyOn(activityService, 'createActivity'); + const deletePermitsByActivitySpy = jest.spyOn(permitService, 'deletePermitsByActivity'); + + it('updates submission with the given activity ID', async () => { + const req = { + body: { activityId: '000000000', submissionId: '11111111' }, + currentUser: CURRENT_USER + }; + const next = jest.fn(); + + updateSubmissionSpy.mockResolvedValue({ activityId: '00000000', submissionId: '11111111' } as Submission); + deletePermitsByActivitySpy.mockResolvedValue(0); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await submissionController.updateDraft(req as any, res as any, next); + + expect(createActivitySpy).toHaveBeenCalledTimes(0); + expect(updateSubmissionSpy).toHaveBeenCalledTimes(1); + expect(deletePermitsByActivitySpy).toHaveBeenCalledTimes(1); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ activityId: '00000000', submissionId: '11111111' }); + }); + + it('populates data from body if it exists', async () => { + const isoPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; + + const req = { + body: { + activityId: '00000000', + submissionId: '11111111', + applicant: { + firstName: 'Test', + lastName: 'User' + }, + basic: { + isDevelopedByCompanyOrOrg: true + }, + housing: { + projectName: 'TheProject' + }, + location: { + projectLocation: 'Some place' + }, + permits: { + hasAppliedProvincialPermits: true + } + }, + currentUser: CURRENT_USER + }; + const next = jest.fn(); + + updateSubmissionSpy.mockResolvedValue({ activityId: '00000000' } as Submission); + deletePermitsByActivitySpy.mockResolvedValue(0); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await submissionController.updateDraft(req as any, res as any, next); + + expect(createActivitySpy).toHaveBeenCalledTimes(0); + expect(updateSubmissionSpy).toHaveBeenCalledTimes(1); + expect(updateSubmissionSpy).toHaveBeenCalledWith( + expect.objectContaining({ + contactName: `${req.body.applicant.firstName} ${req.body.applicant.lastName}`, + isDevelopedByCompanyOrOrg: true, + projectName: 'TheProject', + projectLocation: 'Some place', + hasAppliedProvincialPermits: true, + submissionId: '11111111', + activityId: '00000000', + submittedAt: expect.stringMatching(isoPattern), + intakeStatus: INTAKE_STATUS_LIST.DRAFT, + applicationStatus: APPLICATION_STATUS_LIST.NEW + }) + ); + expect(deletePermitsByActivitySpy).toHaveBeenCalledTimes(1); + }); + + it('sets intake status to Submitted when submit flag given', async () => { + const req = { + body: { + activityId: '00000000', + submissionId: '11111111', + submit: true + }, + currentUser: CURRENT_USER + }; + const next = jest.fn(); + + updateSubmissionSpy.mockResolvedValue({ activityId: '00000000' } as Submission); + deletePermitsByActivitySpy.mockResolvedValue(0); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await submissionController.updateDraft(req as any, res as any, next); + + expect(createActivitySpy).toHaveBeenCalledTimes(0); + expect(updateSubmissionSpy).toHaveBeenCalledTimes(1); + expect(updateSubmissionSpy).toHaveBeenCalledWith( + expect.objectContaining({ + intakeStatus: INTAKE_STATUS_LIST.SUBMITTED + }) + ); + expect(deletePermitsByActivitySpy).toHaveBeenCalledTimes(1); + }); + + it('deletes all existing permits before creating new ones', async () => { + const now = new Date().toISOString(); + + const req = { + body: { + activityId: '00000000', + submissionId: '11111111', + appliedPermits: [ + { + permitTypeId: 1, + trackingId: '123', + status: 'Applied', + statusLastVerified: now + } + ] + }, + currentUser: CURRENT_USER + }; + const next = jest.fn(); + + updateSubmissionSpy.mockResolvedValue({ activityId: '00000000', submissionId: '11111111' } as Submission); + createPermitSpy.mockResolvedValue({} as Permit); + deletePermitsByActivitySpy.mockResolvedValue(0); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await submissionController.updateDraft(req as any, res as any, next); + + const deleteOrder = deletePermitsByActivitySpy.mock.invocationCallOrder[0]; + const createOrder = createPermitSpy.mock.invocationCallOrder[0]; + + expect(createActivitySpy).toHaveBeenCalledTimes(0); + expect(updateSubmissionSpy).toHaveBeenCalledTimes(1); + expect(deletePermitsByActivitySpy).toHaveBeenCalledTimes(1); + expect(createPermitSpy).toHaveBeenCalledTimes(1); + expect(createPermitSpy).toHaveBeenCalledWith( + expect.objectContaining({ + permitTypeId: 1, + activityId: '00000000', + trackingId: '123', + status: 'Applied', + statusLastVerified: now + }) + ); + expect(deleteOrder).toBeLessThan(createOrder); + }); + + it('creates permits if they exist', async () => { + const now = new Date().toISOString(); + + const req = { + body: { + activityId: '00000000', + submissionId: '11111111', + appliedPermits: [ + { + permitTypeId: 1, + trackingId: '123', + status: 'Applied', + statusLastVerified: now + }, + { + permitTypeId: 3, + trackingId: '456', + status: 'Applied', + statusLastVerified: now + } + ], + investigatePermits: [ + { + permitTypeId: 12, + needed: 'Under investigation', + statusLastVerified: now + } + ] + }, + currentUser: CURRENT_USER + }; + const next = jest.fn(); + + updateSubmissionSpy.mockResolvedValue({ activityId: '00000000' } as Submission); + createPermitSpy.mockResolvedValue({} as Permit); + deletePermitsByActivitySpy.mockResolvedValue(0); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await submissionController.updateDraft(req as any, res as any, next); + + expect(createActivitySpy).toHaveBeenCalledTimes(0); + expect(updateSubmissionSpy).toHaveBeenCalledTimes(1); + expect(deletePermitsByActivitySpy).toHaveBeenCalledTimes(1); + expect(createPermitSpy).toHaveBeenCalledTimes(3); + expect(createPermitSpy).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + permitTypeId: 1, + activityId: '00000000', + trackingId: '123', + status: 'Applied', + statusLastVerified: now + }) + ); + expect(createPermitSpy).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + permitTypeId: 3, + activityId: '00000000', + trackingId: '456', + status: 'Applied', + statusLastVerified: now + }) + ); + expect(createPermitSpy).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ + permitTypeId: 12, + activityId: '00000000', + needed: 'Under investigation', + statusLastVerified: now + }) + ); + }); +}); + describe('updateSubmission', () => { const next = jest.fn(); diff --git a/frontend/src/components/form/internal/InputTextInternal.vue b/frontend/src/components/form/internal/InputTextInternal.vue index 08906553..1f00c27e 100644 --- a/frontend/src/components/form/internal/InputTextInternal.vue +++ b/frontend/src/components/form/internal/InputTextInternal.vue @@ -1,11 +1,9 @@