diff --git a/app/src/components/constants.ts b/app/src/components/constants.ts index 47809128..35cb551b 100644 --- a/app/src/components/constants.ts +++ b/app/src/components/constants.ts @@ -42,6 +42,12 @@ export const APPLICATION_STATUS_LIST = Object.freeze({ COMPLETED: 'Completed' }); +export const INTAKE_STATUS_LIST = Object.freeze({ + SUBMITTED: 'Submitted', + ASSIGNED: 'Assigned', + COMPLETED: 'Completed' +}); + export const RENTAL_STATUS_LIST = Object.freeze({ YES: 'Yes', NO: 'No', diff --git a/app/src/controllers/submission.ts b/app/src/controllers/submission.ts index edd03880..1eab45a9 100644 --- a/app/src/controllers/submission.ts +++ b/app/src/controllers/submission.ts @@ -9,6 +9,28 @@ import type { NextFunction, Request, Response } from '../interfaces/IExpress'; import type { ChefsFormConfig, ChefsFormConfigData, Submission, ChefsSubmissionExport, Permit } from '../types'; const controller = { + createEmptySubmission: async (req: Request, res: Response, next: NextFunction) => { + let testSubmissionId; + let submissionQuery; + + // Testing for activityId collisions, which are truncated UUIDs + // If a collision is detected, generate new UUID and test again + do { + testSubmissionId = uuidv4(); + submissionQuery = await submissionService.getSubmission(testSubmissionId.substring(0, 8).toUpperCase()); + } while (submissionQuery); + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const submitter = (req.currentUser?.tokenPayload as any)?.idir_username; + const result = await submissionService.createEmptySubmission(testSubmissionId, submitter); + + res.status(201).json({ activityId: result.activity_id }); + } catch (e: unknown) { + next(e); + } + }, + checkAndStoreNewSubmissions: async () => { const cfg = config.get('server.chefs.forms') as ChefsFormConfig; diff --git a/app/src/routes/v1/submission.ts b/app/src/routes/v1/submission.ts index 47f5a2ba..d50d3f98 100644 --- a/app/src/routes/v1/submission.ts +++ b/app/src/routes/v1/submission.ts @@ -23,6 +23,10 @@ router.get( ); // Submission endpoint +router.put('/create', (req: Request, res: Response, next: NextFunction): void => { + submissionController.createEmptySubmission(req, res, next); +}); + router.get( '/:activityId', submissionValidator.getSubmission, diff --git a/app/src/services/submission.ts b/app/src/services/submission.ts index 51c1a474..191523f8 100644 --- a/app/src/services/submission.ts +++ b/app/src/services/submission.ts @@ -2,7 +2,7 @@ import axios from 'axios'; import config from 'config'; -import { APPLICATION_STATUS_LIST, Initiatives } from '../components/constants'; +import { APPLICATION_STATUS_LIST, INTAKE_STATUS_LIST, Initiatives } from '../components/constants'; import { getChefsApiKey } from '../components/utils'; import prisma from '../db/dataConnection'; import { submission } from '../db/models'; @@ -26,6 +26,40 @@ function chefsAxios(formId: string, options: AxiosRequestConfig = {}): AxiosInst } const service = { + /** + * @function createEmptySubmission + * Creates a new minimal submission + * @returns {Promise>} The result of running the transaction + */ + createEmptySubmission: async (submissionId: string, createdBy: string): Promise> => { + return await prisma.$transaction(async (trx) => { + const initiative = await trx.initiative.findFirstOrThrow({ + where: { + code: Initiatives.HOUSING + } + }); + + const activityId = submissionId.substring(0, 8).toUpperCase() as string; + await trx.activity.create({ + data: { + activity_id: activityId, + initiative_id: initiative.initiative_id + } + }); + + return await trx.submission.create({ + data: { + submission_id: submissionId, + activity_id: activityId, + application_status: APPLICATION_STATUS_LIST.NEW, + intake_status: INTAKE_STATUS_LIST.SUBMITTED, + submitted_at: new Date(Date.now()), + submitted_by: createdBy + } + }); + }); + }, + /** * @function createSubmissionsFromExport * Creates the given activities and submissions from exported CHEFS data diff --git a/app/tests/unit/controllers/submission.spec.ts b/app/tests/unit/controllers/submission.spec.ts index 3e4bb1af..c8f973cd 100644 --- a/app/tests/unit/controllers/submission.spec.ts +++ b/app/tests/unit/controllers/submission.spec.ts @@ -273,6 +273,8 @@ describe('checkAndStoreNewSubmissions', () => { const formExportSpy = jest.spyOn(submissionService, 'getFormExport'); const searchSubmissionsSpy = jest.spyOn(submissionService, 'searchSubmissions'); const createSubmissionsFromExportSpy = jest.spyOn(submissionService, 'createSubmissionsFromExport'); + const createEmptySubmissionSpy = jest.spyOn(submissionService, 'createEmptySubmission'); + const getSubmissionSpy = jest.spyOn(submissionService, 'getSubmission'); it('creates submissions', async () => { (config.get as jest.Mock).mockReturnValueOnce({ @@ -342,6 +344,42 @@ describe('checkAndStoreNewSubmissions', () => { expect(createPermitSpy).toHaveBeenCalledTimes(1); }); + it('creates empty submission with no UUID collision', async () => { + const req = { + body: SUBMISSION_1, + currentUser: CURRENT_USER + }; + const next = jest.fn(); + + createEmptySubmissionSpy.mockResolvedValue({ activity_id: '00000000' }); + getSubmissionSpy.mockResolvedValue(null); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await submissionController.createEmptySubmission(req as any, res as any, next); + + expect(createEmptySubmissionSpy).toHaveBeenCalledTimes(1); + expect(getSubmissionSpy).toHaveBeenCalledTimes(1); + }); + + it('creates empty submission with a UUID collision', async () => { + const req = { + body: SUBMISSION_1, + currentUser: CURRENT_USER + }; + const next = jest.fn(); + + createEmptySubmissionSpy.mockResolvedValue({ activity_id: '11112222' }); + // Mocking two UUID collisions + getSubmissionSpy.mockResolvedValueOnce(SUBMISSION_1); + getSubmissionSpy.mockResolvedValueOnce(SUBMISSION_1); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await submissionController.createEmptySubmission(req as any, res as any, next); + + expect(createEmptySubmissionSpy).toHaveBeenCalledTimes(1); + expect(getSubmissionSpy).toHaveBeenCalledTimes(3); + }); + it('creates permits', async () => { (config.get as jest.Mock).mockReturnValueOnce({ form1: { diff --git a/frontend/src/components/submission/SubmissionList.vue b/frontend/src/components/submission/SubmissionList.vue index 34ba79c4..8050d0f8 100644 --- a/frontend/src/components/submission/SubmissionList.vue +++ b/frontend/src/components/submission/SubmissionList.vue @@ -1,8 +1,10 @@