From 32c2622119c8c5e9b3337a194e3e1dc281438adb Mon Sep 17 00:00:00 2001 From: Morgan Ludtke <42942267+ludtkemorgan@users.noreply.github.com> Date: Wed, 14 Aug 2024 21:25:52 -0500 Subject: [PATCH] fix: add lottery re-run functionality (#4249) --- api/src/services/lottery.service.ts | 12 +- api/test/integration/lottery.e2e-spec.ts | 104 ++++ .../unit/services/lottery.service.spec.ts | 561 ++---------------- 3 files changed, 176 insertions(+), 501 deletions(-) diff --git a/api/src/services/lottery.service.ts b/api/src/services/lottery.service.ts index 97820c268b..47c0c5dd70 100644 --- a/api/src/services/lottery.service.ts +++ b/api/src/services/lottery.service.ts @@ -109,7 +109,6 @@ export class LotteryService { const listing = await this.prisma.listings.findUnique({ select: { id: true, - lotteryLastRunAt: true, lotteryStatus: true, }, where: { @@ -117,6 +116,16 @@ export class LotteryService { }, }); + if (listing?.lotteryStatus) { + // If a lottery has already been run we should delete all of the existing lottery values so that we start from fresh. + // This is needed for two scenarios: + // 1. The lottery generation fails halfway through and the data is corrupted (some values from first run and some from re-reun) - this is very unlikely + // 2. During the regeneration there are now less applications but they are still in the applicationLotteryPositions table + await this.prisma.applicationLotteryPositions.deleteMany({ + where: { listingId: listingId }, + }); + } + try { const applications = await this.prisma.applications.findMany({ select: { @@ -367,7 +376,6 @@ export class LotteryService { ); if (storedListing.status !== ListingsStatusEnum.closed) { - console.log('throwing bc not closed'); throw new BadRequestException( 'Lottery status cannot be changed until listing is closed.', ); diff --git a/api/test/integration/lottery.e2e-spec.ts b/api/test/integration/lottery.e2e-spec.ts index 567cb1638a..cbeec19ae7 100644 --- a/api/test/integration/lottery.e2e-spec.ts +++ b/api/test/integration/lottery.e2e-spec.ts @@ -430,6 +430,110 @@ describe('Lottery Controller Tests', () => { expect(updatedListing.lotteryStatus).toEqual(LotteryStatusEnum.ran); }); + + it('should re-run lottery if lottery has already ran', async () => { + const unitTypeA = await unitTypeFactorySingle( + prisma, + UnitTypeEnum.oneBdrm, + ); + const jurisdiction = await prisma.jurisdictions.create({ + data: jurisdictionFactory(), + }); + await reservedCommunityTypeFactoryAll(jurisdiction.id, prisma); + const listing1 = await listingFactory(jurisdiction.id, prisma, { + status: ListingsStatusEnum.closed, + }); + const listing1Created = await prisma.listings.create({ + data: { + ...listing1, + }, + }); + + const appA = await applicationFactory({ + unitTypeId: unitTypeA.id, + listingId: listing1Created.id, + }); + await prisma.applications.create({ + data: appA, + include: { + applicant: true, + }, + }); + const appB = await applicationFactory({ + unitTypeId: unitTypeA.id, + listingId: listing1Created.id, + }); + await prisma.applications.create({ + data: appB, + include: { + applicant: true, + }, + }); + const appC = await applicationFactory({ + unitTypeId: unitTypeA.id, + listingId: listing1Created.id, + }); + await prisma.applications.create({ + data: appC, + include: { + applicant: true, + }, + }); + const appD = await applicationFactory({ + unitTypeId: unitTypeA.id, + listingId: listing1Created.id, + }); + await prisma.applications.create({ + data: appD, + include: { + applicant: true, + }, + }); + const appE = await applicationFactory({ + unitTypeId: unitTypeA.id, + listingId: listing1Created.id, + }); + await prisma.applications.create({ + data: appE, + include: { + applicant: true, + }, + }); + + await request(app.getHttpServer()) + .put(`/lottery/generateLotteryResults`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .send({ + id: listing1Created.id, + }) + .set('Cookie', cookies) + .expect(200); + const lotterySpotsA = await prisma.applicationLotteryPositions.findMany({ + where: { listingId: listing1Created.id }, + }); + expect(lotterySpotsA).toHaveLength(5); + await request(app.getHttpServer()) + .put(`/lottery/generateLotteryResults`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .send({ + id: listing1Created.id, + }) + .set('Cookie', cookies) + .expect(200); + const lotterySpotsB = await prisma.applicationLotteryPositions.findMany({ + where: { listingId: listing1Created.id }, + }); + expect(lotterySpotsB).toHaveLength(5); + + // The new lottery should be different than the first lottery + const lotterySpotsASorted = lotterySpotsA + .sort((spotA, spotB) => spotA.ordinal - spotB.ordinal) + .map((lotterySpot) => lotterySpot.applicationId); + const lotterySpotsBSorted = lotterySpotsB + .sort((spotA, spotB) => spotA.ordinal - spotB.ordinal) + .map((lotterySpot) => lotterySpot.applicationId); + expect(lotterySpotsASorted).not.toEqual(lotterySpotsBSorted); + }); }); describe('getLotteryResults endpoint', () => { diff --git a/api/test/unit/services/lottery.service.spec.ts b/api/test/unit/services/lottery.service.spec.ts index 6e75ed678f..6c10a919f4 100644 --- a/api/test/unit/services/lottery.service.spec.ts +++ b/api/test/unit/services/lottery.service.spec.ts @@ -7,16 +7,12 @@ import { ReviewOrderTypeEnum, } from '@prisma/client'; import { HttpModule } from '@nestjs/axios'; -import Excel from 'exceljs'; import { Request as ExpressRequest, Response } from 'express'; import { PrismaService } from '../../../src/services/prisma.service'; import { ApplicationCsvExporterService } from '../../../src/services/application-csv-export.service'; import { MultiselectQuestionService } from '../../../src/services/multiselect-question.service'; import { User } from '../../../src/dtos/users/user.dto'; -import { - mockApplication, - mockApplicationSet, -} from './application.service.spec'; +import { mockApplicationSet } from './application.service.spec'; import { mockMultiselectQuestion } from './multiselect-question.service.spec'; import { ListingService } from '../../../src/services/listing.service'; import { PermissionService } from '../../../src/services/permission.service'; @@ -31,10 +27,8 @@ import { Application } from '../../../src/dtos/applications/application.dto'; import MultiselectQuestion from '../../../src/dtos/multiselect-questions/multiselect-question.dto'; import { OrderByEnum } from '../../../src/enums/shared/order-by-enum'; import { LotteryService } from '../../../src/services/lottery.service'; -import { getExportHeaders } from '../../../src/utilities/application-export-helpers'; import { ListingLotteryStatus } from '../../../src/dtos/listings/listing-lottery-status.dto'; import { permissionActions } from '../../../src/enums/permissions/permission-actions-enum'; -import { release } from 'process'; const canOrThrowMock = jest.fn(); const lotteryReleasedMock = jest.fn(); @@ -318,7 +312,6 @@ describe('Testing lottery service', () => { expect(prisma.listings.findUnique).toHaveBeenCalledWith({ select: { id: true, - lotteryLastRunAt: true, lotteryStatus: true, }, where: { @@ -366,513 +359,83 @@ describe('Testing lottery service', () => { }, }); }); - }); - describe('Testing generateSpreadsheetData', () => { - it('should generate spreadsheet and the data', async () => { - const applicationSet = mockApplicationSet(5, new Date(), 0, true); - prisma.applications.findMany = jest - .fn() - .mockResolvedValueOnce(applicationSet); - const workbook = new Excel.Workbook(); + it('should generate lottery results when previous results exist', async () => { const listingId = randomUUID(); - const headers = getExportHeaders( - 0, - [], - 'America/Los_Angeles', - false, - true, - ); - await service.generateSpreadsheetData( - workbook, - applicationSet, - headers, - { - id: listingId, - includeDemographics: false, - timeZone: 'America/Los_Angeles', - }, - true, - ); + const requestingUser = { + firstName: 'requesting fName', + lastName: 'requesting lName', + email: 'requestingUser@email.com', + jurisdictions: [{ id: 'juris id' }], + userRoles: { isAdmin: true }, + } as unknown as User; - expect(prisma.applications.findMany).toBeCalledWith({ - include: { - accessibility: { - select: { - hearing: true, - id: true, - mobility: true, - vision: true, - }, - }, - alternateContact: { - select: { - address: { - select: { - city: true, - county: true, - id: true, - latitude: true, - longitude: true, - placeName: true, - state: true, - street: true, - street2: true, - zipCode: true, - }, - }, - agency: true, - emailAddress: true, - firstName: true, - id: true, - lastName: true, - otherType: true, - phoneNumber: true, - type: true, - }, - }, - applicant: { - select: { - applicantAddress: { - select: { - city: true, - county: true, - id: true, - latitude: true, - longitude: true, - placeName: true, - state: true, - street: true, - street2: true, - zipCode: true, - }, - }, - applicantWorkAddress: { - select: { - city: true, - county: true, - id: true, - latitude: true, - longitude: true, - placeName: true, - state: true, - street: true, - street2: true, - zipCode: true, - }, - }, - birthDay: true, - birthMonth: true, - birthYear: true, - emailAddress: true, - firstName: true, - id: true, - lastName: true, - middleName: true, - noEmail: true, - noPhone: true, - phoneNumber: true, - phoneNumberType: true, - workInRegion: true, - }, - }, - applicationFlaggedSet: { - select: { - id: true, - }, - }, - applicationLotteryPositions: { - select: { - ordinal: true, - }, - where: { - multiselectQuestionId: null, - }, - }, - applicationsAlternateAddress: { - select: { - city: true, - county: true, - id: true, - latitude: true, - longitude: true, - placeName: true, - state: true, - street: true, - street2: true, - zipCode: true, - }, - }, - applicationsMailingAddress: { - select: { - city: true, - county: true, - id: true, - latitude: true, - longitude: true, - placeName: true, - state: true, - street: true, - street2: true, - zipCode: true, - }, - }, - demographics: false, - householdMember: { - select: { - birthDay: true, - birthMonth: true, - birthYear: true, - firstName: true, - householdMemberAddress: { - select: { - city: true, - county: true, - id: true, - latitude: true, - longitude: true, - placeName: true, - state: true, - street: true, - street2: true, - zipCode: true, - }, - }, - householdMemberWorkAddress: { - select: { - city: true, - county: true, - id: true, - latitude: true, - longitude: true, - placeName: true, - state: true, - street: true, - street2: true, - zipCode: true, - }, - }, - id: true, - lastName: true, - middleName: true, - orderId: true, - relationship: true, - sameAddress: true, - workInRegion: true, - }, - }, - listings: false, - preferredUnitTypes: { - select: { - id: true, - name: true, - numBedrooms: true, - }, - }, - userAccounts: { - select: { - email: true, - firstName: true, - id: true, - lastName: true, - }, - }, - }, - where: { - deletedAt: null, - id: { - in: applicationSet.map((appSet) => appSet.id), - }, - listingId: listingId, - markedAsDuplicate: false, - }, + canOrThrowMock.mockResolvedValue(true); + prisma.listings.findUnique = jest.fn().mockResolvedValue({ + id: listingId, + lotteryLastRunAt: new Date(), + lotteryStatus: LotteryStatusEnum.ran, + status: ListingsStatusEnum.closed, }); - expect(workbook.worksheets).toHaveLength(1); - expect(workbook.worksheets[0].columnCount).toEqual(55); - expect(workbook.worksheets[0].rowCount).toEqual(6); // header and 5 applications - expect(workbook.worksheets[0].getColumn(3).header).toEqual( - 'Raw Lottery Rank', - ); - expect(workbook.worksheets[0].getRow(2).getCell(3).value).toEqual('1'); - expect(workbook.worksheets[0].getRow(3).getCell(3).value).toEqual('2'); - expect(workbook.worksheets[0].getRow(4).getCell(3).value).toEqual('3'); - expect(workbook.worksheets[0].getRow(5).getCell(3).value).toEqual('4'); - expect(workbook.worksheets[0].getRow(6).getCell(3).value).toEqual('5'); - }); + const applications = mockApplicationSet(5, new Date()); + prisma.applications.findMany = jest.fn().mockReturnValue(applications); - it('should generate spreadsheet and the data for preference sheet', async () => { - const preferenceId = randomUUID(); - const preference = { name: 'sample preference', id: preferenceId }; - const applicationSet = [ - mockApplication({ - date: new Date(), - position: 2, - numberOfHouseholdMembers: 0, - includeLotteryPosition: true, - preferences: [{ claimed: true, multiselectQuestionId: preferenceId }], - }), - mockApplication({ - date: new Date(), - position: 0, - numberOfHouseholdMembers: 0, - includeLotteryPosition: true, - preferences: [ - { claimed: false, multiselectQuestionId: preferenceId }, - ], - }), - mockApplication({ - date: new Date(), - position: 1, - numberOfHouseholdMembers: 0, - includeLotteryPosition: true, - preferences: [], - }), - mockApplication({ - date: new Date(), - position: 3, - numberOfHouseholdMembers: 0, - includeLotteryPosition: true, - preferences: [{ claimed: true, multiselectQuestionId: preferenceId }], - }), - ]; - prisma.applications.findMany = jest.fn().mockResolvedValueOnce([ + prisma.multiselectQuestions.findMany = jest.fn().mockReturnValue([ { - ...applicationSet[0], - applicationLotteryPositions: [{ ordinal: 1 }], + ...mockMultiselectQuestion( + 0, + new Date(), + MultiselectQuestionsApplicationSectionEnum.preferences, + ), + options: [ + { id: 1, text: 'text' }, + { id: 2, text: 'text', collectAddress: true }, + ], }, { - ...applicationSet[3], - applicationLotteryPositions: [{ ordinal: 2 }], + ...mockMultiselectQuestion( + 1, + new Date(), + MultiselectQuestionsApplicationSectionEnum.programs, + ), + options: [{ id: 1, text: 'text' }], }, - , ]); - const workbook = new Excel.Workbook(); - const listingId = randomUUID(); - const headers = getExportHeaders( - 0, - [], - 'America/Los_Angeles', - false, - true, - ); - await service.generateSpreadsheetData( - workbook, - applicationSet as Application[], - headers, - { - id: listingId, - includeDemographics: false, - timeZone: 'America/Los_Angeles', - }, - true, - preference, + + prisma.applicationLotteryPositions.deleteMany = jest.fn(); + prisma.applicationLotteryPositions.createMany = jest + .fn() + .mockResolvedValue({ id: randomUUID() }); + + prisma.listings.update = jest.fn().mockResolvedValue({ + id: listingId, + lotteryLastRunAt: null, + lotteryStatus: null, + }); + + await service.lotteryGenerate( + { user: requestingUser } as unknown as ExpressRequest, + {} as unknown as Response, + { id: listingId }, ); - expect(prisma.applications.findMany).toBeCalledWith({ - include: { - accessibility: { - select: { - hearing: true, - id: true, - mobility: true, - vision: true, - }, - }, - alternateContact: { - select: { - address: { - select: { - city: true, - county: true, - id: true, - latitude: true, - longitude: true, - placeName: true, - state: true, - street: true, - street2: true, - zipCode: true, - }, - }, - agency: true, - emailAddress: true, - firstName: true, - id: true, - lastName: true, - otherType: true, - phoneNumber: true, - type: true, - }, - }, - applicant: { - select: { - applicantAddress: { - select: { - city: true, - county: true, - id: true, - latitude: true, - longitude: true, - placeName: true, - state: true, - street: true, - street2: true, - zipCode: true, - }, - }, - applicantWorkAddress: { - select: { - city: true, - county: true, - id: true, - latitude: true, - longitude: true, - placeName: true, - state: true, - street: true, - street2: true, - zipCode: true, - }, - }, - birthDay: true, - birthMonth: true, - birthYear: true, - emailAddress: true, - firstName: true, - id: true, - lastName: true, - middleName: true, - noEmail: true, - noPhone: true, - phoneNumber: true, - phoneNumberType: true, - workInRegion: true, - }, - }, - applicationFlaggedSet: { - select: { - id: true, - }, - }, - applicationLotteryPositions: { - select: { - ordinal: true, - }, - where: { - multiselectQuestionId: preferenceId, - }, - }, - applicationsAlternateAddress: { - select: { - city: true, - county: true, - id: true, - latitude: true, - longitude: true, - placeName: true, - state: true, - street: true, - street2: true, - zipCode: true, - }, - }, - applicationsMailingAddress: { - select: { - city: true, - county: true, - id: true, - latitude: true, - longitude: true, - placeName: true, - state: true, - street: true, - street2: true, - zipCode: true, - }, - }, - demographics: false, - householdMember: { - select: { - birthDay: true, - birthMonth: true, - birthYear: true, - firstName: true, - householdMemberAddress: { - select: { - city: true, - county: true, - id: true, - latitude: true, - longitude: true, - placeName: true, - state: true, - street: true, - street2: true, - zipCode: true, - }, - }, - householdMemberWorkAddress: { - select: { - city: true, - county: true, - id: true, - latitude: true, - longitude: true, - placeName: true, - state: true, - street: true, - street2: true, - zipCode: true, - }, - }, - id: true, - lastName: true, - middleName: true, - orderId: true, - relationship: true, - sameAddress: true, - workInRegion: true, - }, - }, - listings: false, - preferredUnitTypes: { - select: { - id: true, - name: true, - numBedrooms: true, - }, - }, - userAccounts: { - select: { - email: true, - firstName: true, - id: true, - lastName: true, - }, - }, + expect(prisma.listings.findUnique).toHaveBeenCalledWith({ + select: { + id: true, + lotteryStatus: true, }, where: { - deletedAt: null, - id: { - in: [applicationSet[0].id, applicationSet[3].id], - }, - listingId: listingId, - markedAsDuplicate: false, + id: listingId, }, }); - expect(workbook.worksheets).toHaveLength(1); - expect(workbook.worksheets[0].columnCount).toEqual(56); - expect(workbook.worksheets[0].rowCount).toEqual(3); // header and 2 applications - expect(workbook.worksheets[0].getColumn(3).header).toEqual( - 'Raw Lottery Rank', - ); - expect(workbook.worksheets[0].getColumn(4).header).toEqual( - 'sample preference Rank', - ); - expect(workbook.worksheets[0].getRow(2).getCell(3).value).toEqual(3); - expect(workbook.worksheets[0].getRow(3).getCell(3).value).toEqual(4); - expect(workbook.worksheets[0].getRow(2).getCell(4).value).toEqual('1'); - expect(workbook.worksheets[0].getRow(3).getCell(4).value).toEqual('2'); + + expect( + prisma.applicationLotteryPositions.deleteMany, + ).toHaveBeenCalledWith({ where: { listingId: listingId } }); + expect(prisma.applications.findMany).toHaveBeenCalled(); + + expect(prisma.applicationLotteryPositions.createMany).toHaveBeenCalled(); + expect(prisma.listings.update).toHaveBeenCalled(); }); });