diff --git a/api/prisma/migrations/42_application_selection_on_delete/migration.sql b/api/prisma/migrations/42_application_selection_on_delete/migration.sql new file mode 100644 index 0000000000..a096454a47 --- /dev/null +++ b/api/prisma/migrations/42_application_selection_on_delete/migration.sql @@ -0,0 +1,11 @@ +-- DropForeignKey +ALTER TABLE "application_selection_options" DROP CONSTRAINT "application_selection_options_application_selection_id_fkey"; + +-- DropForeignKey +ALTER TABLE "application_selections" DROP CONSTRAINT "application_selections_application_id_fkey"; + +-- AddForeignKey +ALTER TABLE "application_selection_options" ADD CONSTRAINT "application_selection_options_application_selection_id_fkey" FOREIGN KEY ("application_selection_id") REFERENCES "application_selections"("id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "application_selections" ADD CONSTRAINT "application_selections_application_id_fkey" FOREIGN KEY ("application_id") REFERENCES "applications"("id") ON DELETE CASCADE ON UPDATE NO ACTION; diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index 0c0e8fa4ed..a2d41d6922 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -182,7 +182,7 @@ model ApplicationSelectionOptions { isGeocodingVerified Boolean? @map("is_geocoding_verified") multiselectOptionId String @map("multiselect_option_id") @db.Uuid addressHolderAddress Address? @relation("application_selection_address", fields: [addressHolderAddressId], references: [id], onDelete: NoAction, onUpdate: NoAction) - applicationSelections ApplicationSelections @relation(fields: [applicationSelectionId], references: [id], onDelete: NoAction, onUpdate: NoAction) + applicationSelection ApplicationSelections @relation(fields: [applicationSelectionId], references: [id], onDelete: Cascade, onUpdate: NoAction) multiselectOption MultiselectOptions @relation(fields: [multiselectOptionId], references: [id], onDelete: NoAction, onUpdate: NoAction) @@map("application_selection_options") @@ -195,7 +195,7 @@ model ApplicationSelections { applicationId String @map("application_id") @db.Uuid hasOptedOut Boolean? @map("has_opted_out") multiselectQuestionId String @map("multiselect_question_id") @db.Uuid - application Applications @relation(fields: [applicationId], references: [id], onDelete: NoAction, onUpdate: NoAction) + application Applications @relation(fields: [applicationId], references: [id], onDelete: Cascade, onUpdate: NoAction) multiselectQuestion MultiselectQuestions @relation(fields: [multiselectQuestionId], references: [id], onDelete: NoAction, onUpdate: NoAction) selections ApplicationSelectionOptions[] diff --git a/api/src/dtos/addresses/address-update.dto.ts b/api/src/dtos/addresses/address-update.dto.ts new file mode 100644 index 0000000000..00ab5adbdf --- /dev/null +++ b/api/src/dtos/addresses/address-update.dto.ts @@ -0,0 +1,17 @@ +import { Expose } from 'class-transformer'; +import { IsString, IsUUID } from 'class-validator'; +import { ApiPropertyOptional, OmitType } from '@nestjs/swagger'; +import { Address } from './address.dto'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; + +export class AddressUpdate extends OmitType(Address, [ + 'id', + 'createdAt', + 'updatedAt', +]) { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() + id?: string; +} diff --git a/api/src/dtos/applications/application-create.dto.ts b/api/src/dtos/applications/application-create.dto.ts index 32b5dc1b76..d272c35471 100644 --- a/api/src/dtos/applications/application-create.dto.ts +++ b/api/src/dtos/applications/application-create.dto.ts @@ -1,4 +1,23 @@ -import { OmitType } from '@nestjs/swagger'; +import { Expose, Type } from 'class-transformer'; +import { ArrayMaxSize, ValidateNested } from 'class-validator'; +import { ApiPropertyOptional, OmitType } from '@nestjs/swagger'; import { ApplicationUpdate } from './application-update.dto'; +import { ApplicationSelectionCreate } from './application-selection-create.dto'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -export class ApplicationCreate extends OmitType(ApplicationUpdate, ['id']) {} +export class ApplicationCreate extends OmitType(ApplicationUpdate, [ + 'id', + 'applicationSelections', +]) { + // TODO: Temporarily optional until after MSQ refactor + @Expose() + // @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ArrayMaxSize(64, { groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ApplicationSelectionCreate) + @ApiPropertyOptional({ + type: ApplicationSelectionCreate, + isArray: true, + }) + applicationSelections?: ApplicationSelectionCreate[]; +} diff --git a/api/src/dtos/applications/application-selection-create.dto.ts b/api/src/dtos/applications/application-selection-create.dto.ts new file mode 100644 index 0000000000..a98534acba --- /dev/null +++ b/api/src/dtos/applications/application-selection-create.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty, OmitType } from '@nestjs/swagger'; +import { Expose, Type } from 'class-transformer'; +import { IsDefined, ValidateNested } from 'class-validator'; +import { ApplicationSelectionUpdate } from './application-selection-update.dto'; +import { ApplicationSelectionOptionCreate } from './application-selection-option-create.dto'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; + +export class ApplicationSelectionCreate extends OmitType( + ApplicationSelectionUpdate, + ['id', 'application', 'selections'], +) { + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ApplicationSelectionOptionCreate) + @ApiProperty({ + type: ApplicationSelectionOptionCreate, + isArray: true, + }) + selections: ApplicationSelectionOptionCreate[]; +} diff --git a/api/src/dtos/applications/application-selection-option-create.dto.ts b/api/src/dtos/applications/application-selection-option-create.dto.ts new file mode 100644 index 0000000000..227f1e383e --- /dev/null +++ b/api/src/dtos/applications/application-selection-option-create.dto.ts @@ -0,0 +1,7 @@ +import { OmitType } from '@nestjs/swagger'; +import { ApplicationSelectionOptionUpdate } from './application-selection-option-update.dto'; + +export class ApplicationSelectionOptionCreate extends OmitType( + ApplicationSelectionOptionUpdate, + ['id'], +) {} diff --git a/api/src/dtos/applications/application-selection-option-update.dto.ts b/api/src/dtos/applications/application-selection-option-update.dto.ts new file mode 100644 index 0000000000..a37355bcf1 --- /dev/null +++ b/api/src/dtos/applications/application-selection-option-update.dto.ts @@ -0,0 +1,36 @@ +import { ApiPropertyOptional, OmitType } from '@nestjs/swagger'; +import { Expose, Type } from 'class-transformer'; +import { IsString, IsUUID, ValidateNested } from 'class-validator'; +import { ApplicationSelectionOption } from './application-selection-option.dto'; +import { AddressUpdate } from '../addresses/address-update.dto'; +import { IdDTO } from '../shared/id.dto'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; + +export class ApplicationSelectionOptionUpdate extends OmitType( + ApplicationSelectionOption, + [ + 'id', + 'createdAt', + 'updatedAt', + 'addressHolderAddress', + 'applicationSelection', + ], +) { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() + id?: string; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AddressUpdate) + @ApiPropertyOptional({ type: AddressUpdate }) + addressHolderAddress?: AddressUpdate; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => IdDTO) + @ApiPropertyOptional({ type: IdDTO }) + applicationSelection?: IdDTO; +} diff --git a/api/src/dtos/applications/application-selection-options.dto.ts b/api/src/dtos/applications/application-selection-option.dto.ts similarity index 81% rename from api/src/dtos/applications/application-selection-options.dto.ts rename to api/src/dtos/applications/application-selection-option.dto.ts index 05a8fffe1c..cf90199538 100644 --- a/api/src/dtos/applications/application-selection-options.dto.ts +++ b/api/src/dtos/applications/application-selection-option.dto.ts @@ -9,14 +9,15 @@ import { import { AbstractDTO } from '../shared/abstract.dto'; import { IdDTO } from '../shared/id.dto'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { Address } from '../addresses/address.dto'; -class ApplicationSelectionOptions extends AbstractDTO { +export class ApplicationSelectionOption extends AbstractDTO { @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => IdDTO) - @ApiProperty({ type: IdDTO }) - addressHolderAddress?: IdDTO; + @Type(() => Address) + @ApiProperty({ type: Address }) + addressHolderAddress?: Address; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @@ -42,10 +43,7 @@ class ApplicationSelectionOptions extends AbstractDTO { @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => IdDTO) @ApiProperty({ type: IdDTO }) multiselectOption: IdDTO; } - -export { ApplicationSelectionOptions as default, ApplicationSelectionOptions }; diff --git a/api/src/dtos/applications/application-selection-update.dto.ts b/api/src/dtos/applications/application-selection-update.dto.ts new file mode 100644 index 0000000000..5b2f7cea9f --- /dev/null +++ b/api/src/dtos/applications/application-selection-update.dto.ts @@ -0,0 +1,29 @@ +import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger'; +import { Expose, Type } from 'class-transformer'; +import { IsDefined, IsString, IsUUID, ValidateNested } from 'class-validator'; +import { ApplicationSelection } from './application-selection.dto'; +import { ApplicationSelectionOptionUpdate } from './application-selection-option-update.dto'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; + +export class ApplicationSelectionUpdate extends OmitType(ApplicationSelection, [ + 'id', + 'createdAt', + 'updatedAt', + 'selections', +]) { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() + id?: string; + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ApplicationSelectionOptionUpdate) + @ApiProperty({ + type: ApplicationSelectionOptionUpdate, + isArray: true, + }) + selections: ApplicationSelectionOptionUpdate[]; +} diff --git a/api/src/dtos/applications/application-selections.dto.ts b/api/src/dtos/applications/application-selection.dto.ts similarity index 71% rename from api/src/dtos/applications/application-selections.dto.ts rename to api/src/dtos/applications/application-selection.dto.ts index b7cf4797dc..96ac8cd26d 100644 --- a/api/src/dtos/applications/application-selections.dto.ts +++ b/api/src/dtos/applications/application-selection.dto.ts @@ -1,12 +1,12 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Expose, Type } from 'class-transformer'; import { ValidateNested, IsBoolean, IsDefined } from 'class-validator'; -import ApplicationSelectionOptions from './application-selection-options.dto'; +import { ApplicationSelectionOption } from './application-selection-option.dto'; import { AbstractDTO } from '../shared/abstract.dto'; import { IdDTO } from '../shared/id.dto'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -class ApplicationSelections extends AbstractDTO { +export class ApplicationSelection extends AbstractDTO { @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @@ -21,7 +21,6 @@ class ApplicationSelections extends AbstractDTO { @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => IdDTO) @ApiProperty({ type: IdDTO }) multiselectQuestion: IdDTO; @@ -29,9 +28,7 @@ class ApplicationSelections extends AbstractDTO { @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => ApplicationSelectionOptions) - @ApiProperty({ type: ApplicationSelectionOptions }) - selections: ApplicationSelectionOptions[]; + @Type(() => ApplicationSelectionOption) + @ApiProperty({ type: ApplicationSelectionOption }) + selections: ApplicationSelectionOption[]; } - -export { ApplicationSelections as default, ApplicationSelections }; diff --git a/api/src/dtos/applications/application-update.dto.ts b/api/src/dtos/applications/application-update.dto.ts index 71a2222411..b143b492a7 100644 --- a/api/src/dtos/applications/application-update.dto.ts +++ b/api/src/dtos/applications/application-update.dto.ts @@ -1,39 +1,65 @@ -import { ApiProperty, OmitType } from '@nestjs/swagger'; import { Expose, Type } from 'class-transformer'; import { ArrayMaxSize, ValidateNested } from 'class-validator'; -import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -import { AddressCreate } from '../addresses/address-create.dto'; +import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger'; import { AccessibilityUpdate } from './accessibility-update.dto'; import { AlternateContactUpdate } from './alternate-contact-update.dto'; import { ApplicantUpdate } from './applicant-update.dto'; import { Application } from './application.dto'; +import { ApplicationSelectionUpdate } from './application-selection-update.dto'; import { DemographicUpdate } from './demographic-update.dto'; import { HouseholdMemberUpdate } from './household-member-update.dto'; +import { AddressCreate } from '../addresses/address-create.dto'; import { IdDTO } from '../shared/id.dto'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; export class ApplicationUpdate extends OmitType(Application, [ 'createdAt', 'updatedAt', 'deletedAt', + 'accessibility', + 'alternateContact', 'applicant', - 'applicationsMailingAddress', + 'applicationLotteryPositions', + 'applicationSelections', 'applicationsAlternateAddress', - 'alternateContact', - 'accessibility', + 'applicationsMailingAddress', + 'confirmationCode', 'demographics', + 'flagged', 'householdMember', 'markedAsDuplicate', - 'flagged', - 'confirmationCode', 'preferredUnitTypes', - 'applicationLotteryPositions', ]) { + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AccessibilityUpdate) + @ApiProperty({ type: AccessibilityUpdate }) + accessibility: AccessibilityUpdate; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AlternateContactUpdate) + @ApiProperty({ type: AlternateContactUpdate }) + alternateContact: AlternateContactUpdate; + @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => ApplicantUpdate) @ApiProperty({ type: ApplicantUpdate }) applicant: ApplicantUpdate; + // TODO: Temporarily optional until after MSQ refactor + @Expose() + // @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ArrayMaxSize(64, { groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ApplicationSelectionUpdate) + @ApiPropertyOptional({ + type: ApplicationSelectionUpdate, + isArray: true, + }) + applicationSelections?: ApplicationSelectionUpdate[]; + @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => AddressCreate) @@ -46,18 +72,6 @@ export class ApplicationUpdate extends OmitType(Application, [ @ApiProperty({ type: AddressCreate }) applicationsAlternateAddress: AddressCreate; - @Expose() - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => AlternateContactUpdate) - @ApiProperty({ type: AlternateContactUpdate }) - alternateContact: AlternateContactUpdate; - - @Expose() - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => AccessibilityUpdate) - @ApiProperty({ type: AccessibilityUpdate }) - accessibility: AccessibilityUpdate; - @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => DemographicUpdate) diff --git a/api/src/dtos/applications/application.dto.ts b/api/src/dtos/applications/application.dto.ts index eb2bc072bb..3f86931386 100644 --- a/api/src/dtos/applications/application.dto.ts +++ b/api/src/dtos/applications/application.dto.ts @@ -20,19 +20,19 @@ import { MaxLength, ValidateNested, } from 'class-validator'; -import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -import { Address } from '../addresses/address.dto'; -import { AbstractDTO } from '../shared/abstract.dto'; -import { IdDTO } from '../shared/id.dto'; import { Accessibility } from './accessibility.dto'; import { AlternateContact } from './alternate-contact.dto'; import { Applicant } from './applicant.dto'; +import { ApplicationLotteryPosition } from './application-lottery-position.dto'; import { ApplicationMultiselectQuestion } from './application-multiselect-question.dto'; +import { ApplicationSelection } from './application-selection.dto'; import { Demographic } from './demographic.dto'; import { HouseholdMember } from './household-member.dto'; +import { Address } from '../addresses/address.dto'; +import { AbstractDTO } from '../shared/abstract.dto'; +import { IdDTO } from '../shared/id.dto'; import { UnitType } from '../unit-types/unit-type.dto'; -import { ApplicationLotteryPosition } from './application-lottery-position.dto'; -import ApplicationSelections from './application-selections.dto'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; export class Application extends AbstractDTO { @Expose() @@ -262,16 +262,16 @@ export class Application extends AbstractDTO { householdMember: HouseholdMember[]; // TODO: Temporarily optional until after MSQ refactor - // @Expose() + @Expose() // @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @ArrayMaxSize(64, { groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @Type(() => ApplicationSelections) + @Type(() => ApplicationSelection) @ApiPropertyOptional({ - type: ApplicationSelections, + type: ApplicationSelection, isArray: true, }) - applicationSelections?: ApplicationSelections[]; + applicationSelections?: ApplicationSelection[]; @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) diff --git a/api/src/services/application.service.ts b/api/src/services/application.service.ts index f913d46e3e..2bef44c199 100644 --- a/api/src/services/application.service.ts +++ b/api/src/services/application.service.ts @@ -1,46 +1,51 @@ -import { - BadRequestException, - Injectable, - NotFoundException, - ForbiddenException, - Logger, - Inject, -} from '@nestjs/common'; import crypto from 'crypto'; import dayjs from 'dayjs'; import { Request as ExpressRequest } from 'express'; import { + ApplicationSelections, ListingEventsTypeEnum, ListingsStatusEnum, LotteryStatusEnum, Prisma, YesNoEnum, } from '@prisma/client'; +import { + BadRequestException, + Injectable, + NotFoundException, + ForbiddenException, + Logger, + Inject, + HttpException, +} from '@nestjs/common'; +import { EmailService } from './email.service'; +import { GeocodingService } from './geocoding.service'; +import { PermissionService } from './permission.service'; import { PrismaService } from './prisma.service'; import { Application } from '../dtos/applications/application.dto'; -import { mapTo } from '../utilities/mapTo'; +import { ApplicationCreate } from '../dtos/applications/application-create.dto'; import { ApplicationQueryParams } from '../dtos/applications/application-query-params.dto'; -import { calculateSkip, calculateTake } from '../utilities/pagination-helpers'; -import { buildOrderByForApplications } from '../utilities/build-order-by'; -import { buildPaginationInfo } from '../utilities/build-pagination-meta'; -import { IdDTO } from '../dtos/shared/id.dto'; -import { SuccessDTO } from '../dtos/shared/success.dto'; -import { ApplicationViews } from '../enums/applications/view-enum'; +import { ApplicationSelectionCreate } from '../dtos/applications/application-selection-create.dto'; import { ApplicationUpdate } from '../dtos/applications/application-update.dto'; -import { ApplicationCreate } from '../dtos/applications/application-create.dto'; +import { MostRecentApplicationQueryParams } from '../dtos/applications/most-recent-application-query-params.dto'; import { PaginatedApplicationDto } from '../dtos/applications/paginated-application.dto'; -import { EmailService } from './email.service'; -import { PermissionService } from './permission.service'; +import { PublicAppsViewQueryParams } from '../dtos/applications/public-apps-view-params.dto'; +import { PublicAppsViewResponse } from '../dtos/applications/public-apps-view-response.dto'; +import { Jurisdiction } from '../dtos/jurisdictions/jurisdiction.dto'; import Listing from '../dtos/listings/listing.dto'; +import { IdDTO } from '../dtos/shared/id.dto'; +import { SuccessDTO } from '../dtos/shared/success.dto'; import { User } from '../dtos/users/user.dto'; -import { permissionActions } from '../enums/permissions/permission-actions-enum'; -import { GeocodingService } from './geocoding.service'; -import { MostRecentApplicationQueryParams } from '../dtos/applications/most-recent-application-query-params.dto'; -import { PublicAppsViewQueryParams } from '../dtos/applications/public-apps-view-params.dto'; import { ApplicationsFilterEnum } from '../enums/applications/filter-enum'; -import { PublicAppsViewResponse } from '../dtos/applications/public-apps-view-response.dto'; import { CronJobService } from './cron-job.service'; -import { DefaultArgs } from '@prisma/client/runtime/library'; +import { ApplicationViews } from '../enums/applications/view-enum'; +import { FeatureFlagEnum } from '../enums/feature-flags/feature-flags-enum'; +import { permissionActions } from '../enums/permissions/permission-actions-enum'; +import { buildOrderByForApplications } from '../utilities/build-order-by'; +import { buildPaginationInfo } from '../utilities/build-pagination-meta'; +import { doJurisdictionHaveFeatureFlagSet } from '../utilities/feature-flag-utilities'; +import { mapTo } from '../utilities/mapTo'; +import { calculateSkip, calculateTake } from '../utilities/pagination-helpers'; export const view: Partial< Record @@ -188,6 +193,30 @@ export const view: Partial< view.base = { ...view.partnerList, + applicationSelections: { + include: { + multiselectQuestion: true, + selections: { + include: { + addressHolderAddress: { + select: { + id: true, + placeName: true, + city: true, + county: true, + state: true, + street: true, + street2: true, + zipCode: true, + latitude: true, + longitude: true, + }, + }, + multiselectOption: true, + }, + }, + }, + }, demographics: { select: { id: true, @@ -210,6 +239,7 @@ view.base = { name: true, }, }, + listingMultiselectQuestions: true, }, }, householdMember: { @@ -273,7 +303,7 @@ view.details = { const PII_DELETION_CRON_JOB_NAME = 'PII_DELETION_CRON_STRING'; /* - this is the service for applicationss + this is the service for applications it handles all the backend's business logic for reading/writing/deleting application data */ @Injectable() @@ -625,18 +655,39 @@ export class ApplicationService { id: dto.listings.id, }, include: { - jurisdictions: true, - // support unit group availability logic in email - unitGroups: true, - // multiselect questions and address is needed for geocoding + jurisdictions: { include: { featureFlags: true } }, + // address is needed for geocoding + listingsBuildingAddress: true, listingMultiselectQuestions: { - include: { - multiselectQuestions: true, - }, + include: { multiselectQuestions: true }, }, - listingsBuildingAddress: true, + // support unit group availability logic in email + unitGroups: true, }, }); + + const enableV2MSQ = doJurisdictionHaveFeatureFlagSet( + listing?.jurisdictions as unknown as Jurisdiction, + FeatureFlagEnum.enableV2MSQ, + ); + + if (enableV2MSQ) { + const listingMultiselectIds = listing.listingMultiselectQuestions.map( + (msq) => { + return msq.multiselectQuestionId; + }, + ); + if ( + !dto.applicationSelections.every(({ multiselectQuestion }) => { + return listingMultiselectIds.includes(multiselectQuestion.id); + }) + ) { + throw new BadRequestException( + 'Application selections contain multiselect question ids not present on the listing', + ); + } + } + // if its a public submission if (forPublic) { // SubmissionDate is time the application was created for public @@ -679,8 +730,25 @@ export class ApplicationService { this.prisma.applications.create({ data: { ...dto, - isNewest: !!requestingUser?.id && forPublic, - confirmationCode: this.generateConfirmationCode(), + accessibility: dto.accessibility + ? { + create: { + ...dto.accessibility, + }, + } + : undefined, + alternateContact: dto.alternateContact + ? { + create: { + ...dto.alternateContact, + address: { + create: { + ...dto.alternateContact.address, + }, + }, + }, + } + : undefined, applicant: dto.applicant ? { create: { @@ -710,25 +778,7 @@ export class ApplicationService { }, } : undefined, - accessibility: dto.accessibility - ? { - create: { - ...dto.accessibility, - }, - } - : undefined, - alternateContact: dto.alternateContact - ? { - create: { - ...dto.alternateContact, - address: { - create: { - ...dto.alternateContact.address, - }, - }, - }, - } - : undefined, + applicationSelections: undefined, applicationsAlternateAddress: dto.applicationsAlternateAddress ? { create: { @@ -743,13 +793,7 @@ export class ApplicationService { }, } : undefined, - listings: dto.listings - ? { - connect: { - id: dto.listings.id, - }, - } - : undefined, + confirmationCode: this.generateConfirmationCode(), demographics: dto.demographics ? { create: { @@ -757,13 +801,7 @@ export class ApplicationService { }, } : undefined, - preferredUnitTypes: dto.preferredUnitTypes - ? { - connect: dto.preferredUnitTypes.map((unitType) => ({ - id: unitType.id, - })), - } - : undefined, + expireAfter: expireAfterDate, householdMember: dto.householdMember ? { create: dto.householdMember.map((member) => ({ @@ -795,8 +833,23 @@ export class ApplicationService { })), } : undefined, - programs: dto.programs as unknown as Prisma.JsonArray, + isNewest: !!requestingUser?.id && forPublic, + listings: dto.listings + ? { + connect: { + id: dto.listings.id, + }, + } + : undefined, preferences: dto.preferences as unknown as Prisma.JsonArray, + preferredUnitTypes: dto.preferredUnitTypes + ? { + connect: dto.preferredUnitTypes.map((unitType) => ({ + id: unitType.id, + })), + } + : undefined, + programs: dto.programs as unknown as Prisma.JsonArray, userAccounts: requestingUser ? { connect: { @@ -804,10 +857,6 @@ export class ApplicationService { }, } : undefined, - expireAfter: expireAfterDate, - - // TODO: Temporary until after MSQ refactor - applicationSelections: undefined, }, include: view.details, }), @@ -816,6 +865,33 @@ export class ApplicationService { const prismaTransactions = await this.prisma.$transaction(transactions); const rawApplication = prismaTransactions[prismaTransactions.length - 1]; + if (!rawApplication) { + throw new HttpException('Application failed to save', 500); + } + + const rawSelections = []; + if (enableV2MSQ) { + // Nested CreateManys are not supported by Prisma, + // thus we must create the subobjects after creating the application + try { + for (const selection of dto.applicationSelections) { + const rawSelection = await this.createApplicationSelection( + selection, + rawApplication.id, + ); + rawSelections.push(rawSelection); + } + } catch (error) { + // On error, all associated records should be delete. + // Deleting the application will cascade delete the other records + await this.prisma.applications.delete({ + where: { id: rawApplication.id }, + }); + throw new BadRequestException(error); + } + } + rawApplication.applicationSelections = rawSelections; + const mappedApplication = mapTo(Application, rawApplication); if (dto.applicant.emailAddress && forPublic) { this.emailService.applicationConfirmation( @@ -828,8 +904,9 @@ export class ApplicationService { await this.updateListingApplicationEditTimestamp(listing.id); // Calculate geocoding preferences after save and email sent - if (listing.jurisdictions?.enableGeocodingPreferences) { + if (!enableV2MSQ && listing.jurisdictions?.enableGeocodingPreferences) { try { + // TODO: Rewrite for V2MSQ void this.geocodingService.validateGeocodingPreferences( mappedApplication, mapTo(Listing, listing), @@ -852,160 +929,184 @@ export class ApplicationService { dto: ApplicationUpdate, requestingUser: User, ): Promise { - const rawApplication = await this.findOrThrow(dto.id); + const rawExistingApplication = await this.findOrThrow( + dto.id, + ApplicationViews.base, + ); await this.authorizeAction( requestingUser, - rawApplication.listingId, + rawExistingApplication.listingId, permissionActions.update, ); - // All connected household members should be deleted so they can be recreated in the update below. - // This solves for all cases of deleted members, updated members, and new members - await this.prisma.householdMember.deleteMany({ + const listing = await this.prisma.listings.findUnique({ where: { - applicationId: dto.id, + id: dto.listings.id, + }, + include: { + jurisdictions: { include: { featureFlags: true } }, + listingMultiselectQuestions: { + include: { multiselectQuestions: true }, + }, }, }); - const res = await this.prisma.applications.update({ - where: { - id: dto.id, - }, - include: view.details, - data: { - ...dto, - id: undefined, - applicant: dto.applicant - ? { - create: { - ...dto.applicant, - applicantAddress: { - create: { - ...dto.applicant.applicantAddress, - }, + const transactions = []; + + // All connected household members should be deleted so they can be recreated in the update below. + // This solves for all cases of deleted members, updated members, and new members + transactions.push( + this.prisma.householdMember.deleteMany({ + where: { + applicationId: dto.id, + }, + }), + ); + + transactions.push( + this.prisma.applications.update({ + where: { + id: dto.id, + }, + include: view.details, + data: { + ...dto, + id: undefined, + accessibility: dto.accessibility + ? { + create: { + ...dto.accessibility, }, - applicantWorkAddress: { - create: { - ...dto.applicant.applicantWorkAddress, + } + : undefined, + alternateContact: dto.alternateContact + ? { + create: { + ...dto.alternateContact, + address: { + create: { + ...dto.alternateContact.address, + }, }, }, - firstName: dto.applicant.firstName?.trim(), - lastName: dto.applicant.lastName?.trim(), - birthDay: dto.applicant.birthDay - ? Number(dto.applicant.birthDay) - : undefined, - birthMonth: dto.applicant.birthMonth - ? Number(dto.applicant.birthMonth) - : undefined, - birthYear: dto.applicant.birthYear - ? Number(dto.applicant.birthYear) - : undefined, - fullTimeStudent: dto.applicant.fullTimeStudent, - }, - } - : undefined, - accessibility: dto.accessibility - ? { - create: { - ...dto.accessibility, - }, - } - : undefined, - alternateContact: dto.alternateContact - ? { - create: { - ...dto.alternateContact, - address: { - create: { - ...dto.alternateContact.address, + } + : undefined, + applicant: dto.applicant + ? { + create: { + ...dto.applicant, + applicantAddress: { + create: { + ...dto.applicant.applicantAddress, + }, }, - }, - }, - } - : undefined, - applicationsAlternateAddress: dto.applicationsAlternateAddress - ? { - create: { - ...dto.applicationsAlternateAddress, - }, - } - : undefined, - applicationsMailingAddress: dto.applicationsMailingAddress - ? { - create: { - ...dto.applicationsMailingAddress, - }, - } - : undefined, - listings: dto.listings - ? { - connect: { - id: dto.listings.id, - }, - } - : undefined, - demographics: dto.demographics - ? { - create: { - ...dto.demographics, - }, - } - : undefined, - preferredUnitTypes: dto.preferredUnitTypes - ? { - set: dto.preferredUnitTypes.map((unitType) => ({ - id: unitType.id, - })), - } - : undefined, - householdMember: dto.householdMember - ? { - create: dto.householdMember.map((member) => ({ - ...member, - sameAddress: member.sameAddress || YesNoEnum.no, - workInRegion: member.workInRegion || YesNoEnum.no, - householdMemberAddress: { - create: { - ...member.householdMemberAddress, + applicantWorkAddress: { + create: { + ...dto.applicant.applicantWorkAddress, + }, }, + firstName: dto.applicant.firstName?.trim(), + lastName: dto.applicant.lastName?.trim(), + birthDay: dto.applicant.birthDay + ? Number(dto.applicant.birthDay) + : undefined, + birthMonth: dto.applicant.birthMonth + ? Number(dto.applicant.birthMonth) + : undefined, + birthYear: dto.applicant.birthYear + ? Number(dto.applicant.birthYear) + : undefined, + fullTimeStudent: dto.applicant.fullTimeStudent, + }, + } + : undefined, + applicationSelections: dto.applicationSelections ? {} : undefined, + applicationsAlternateAddress: dto.applicationsAlternateAddress + ? { + create: { + ...dto.applicationsAlternateAddress, + }, + } + : undefined, + applicationsMailingAddress: dto.applicationsMailingAddress + ? { + create: { + ...dto.applicationsMailingAddress, + }, + } + : undefined, + demographics: dto.demographics + ? { + create: { + ...dto.demographics, }, - householdMemberWorkAddress: { - create: { - ...member.householdMemberWorkAddress, + } + : undefined, + householdMember: dto.householdMember + ? { + create: dto.householdMember.map((member) => ({ + ...member, + sameAddress: member.sameAddress || YesNoEnum.no, + workInRegion: member.workInRegion || YesNoEnum.no, + householdMemberAddress: { + create: { + ...member.householdMemberAddress, + }, + }, + householdMemberWorkAddress: { + create: { + ...member.householdMemberWorkAddress, + }, }, + firstName: member.firstName?.trim(), + lastName: member.lastName?.trim(), + birthDay: member.birthDay + ? Number(member.birthDay) + : undefined, + birthMonth: member.birthMonth + ? Number(member.birthMonth) + : undefined, + birthYear: member.birthYear + ? Number(member.birthYear) + : undefined, + fullTimeStudent: member.fullTimeStudent, + })), + } + : undefined, + listings: dto.listings + ? { + connect: { + id: dto.listings.id, }, - firstName: member.firstName?.trim(), - lastName: member.lastName?.trim(), - birthDay: member.birthDay ? Number(member.birthDay) : undefined, - birthMonth: member.birthMonth - ? Number(member.birthMonth) - : undefined, - birthYear: member.birthYear - ? Number(member.birthYear) - : undefined, - fullTimeStudent: member.fullTimeStudent, - })), - } - : undefined, - programs: dto.programs as unknown as Prisma.JsonArray, - preferences: dto.preferences as unknown as Prisma.JsonArray, - - // TODO: Temporary until after MSQ refactor - applicationSelections: undefined, - }, - }); + } + : undefined, + preferredUnitTypes: dto.preferredUnitTypes + ? { + set: dto.preferredUnitTypes.map((unitType) => ({ + id: unitType.id, + })), + } + : undefined, - const listing = await this.prisma.listings.findFirst({ - where: { id: dto.listings.id }, - include: { - jurisdictions: true, - listingMultiselectQuestions: { - include: { multiselectQuestions: true }, + // TODO: Can be removed after MSQ refactor + preferences: dto.preferences as unknown as Prisma.JsonArray, + programs: dto.programs as unknown as Prisma.JsonArray, }, - }, - }); - const application = mapTo(Application, res); + }), + ); + + const prismaTransactions = await this.prisma.$transaction(transactions); + const rawApplication = prismaTransactions[prismaTransactions.length - 1]; + + if (!rawApplication) { + throw new HttpException( + `Application ${rawExistingApplication.id} failed to update`, + 500, + ); + } + + const application = mapTo(Application, rawApplication); // Calculate geocoding preferences after save and email sent if (listing?.jurisdictions?.enableGeocodingPreferences) { @@ -1021,7 +1122,7 @@ export class ApplicationService { } } - await this.updateListingApplicationEditTimestamp(res.listingId); + await this.updateListingApplicationEditTimestamp(rawApplication.listingId); return application; } @@ -1056,7 +1157,7 @@ export class ApplicationService { } /* - finds the requested listing or throws an error + finds the requested application or throws an error */ async findOrThrow(applicationId: string, includeView?: ApplicationViews) { const res = await this.prisma.applications.findUnique({ @@ -1304,4 +1405,68 @@ export class ApplicationService { generateConfirmationCode(): string { return crypto.randomBytes(4).toString('hex').toUpperCase(); } + + async createApplicationSelection( + selection: ApplicationSelectionCreate, + applicationId: string, + ): Promise { + const selectedOptions = []; + + for (const selectionOption of selection.selections) { + let address; + // If an address is passed, create the address for the selection option + if (selectionOption.addressHolderAddress) { + address = await this.prisma.address.create({ + data: { + ...selectionOption.addressHolderAddress, + }, + }); + } + // Build the create selection option body + const selectedOptionBody = { + addressHolderAddressId: address?.id, + addressHolderName: selectionOption.addressHolderName, + addressHolderRelationship: selectionOption.addressHolderRelationship, + isGeocodingVerified: selectionOption.isGeocodingVerified, + multiselectOptionId: selectionOption.multiselectOption.id, + }; + // Push the selection option to a list for the createMany + selectedOptions.push(selectedOptionBody); + } + // Create the application selection with nested createMany applicationSelectionOptions + return await this.prisma.applicationSelections.create({ + data: { + applicationId: applicationId, + hasOptedOut: selection.hasOptedOut ?? false, + multiselectQuestionId: selection.multiselectQuestion.id, + selections: { + createMany: { + data: selectedOptions, + }, + }, + }, + include: { + multiselectQuestion: true, + selections: { + include: { + addressHolderAddress: { + select: { + id: true, + placeName: true, + city: true, + county: true, + state: true, + street: true, + street2: true, + zipCode: true, + latitude: true, + longitude: true, + }, + }, + multiselectOption: true, + }, + }, + }, + }); + } } diff --git a/api/src/services/email.service.ts b/api/src/services/email.service.ts index a274e8e6db..ffcfe4cc8c 100644 --- a/api/src/services/email.service.ts +++ b/api/src/services/email.service.ts @@ -347,7 +347,7 @@ export class EmailService { public async applicationConfirmation( listing: Listing, - application: ApplicationCreate, + application: Application, appUrl: string, ) { const jurisdiction = await this.getJurisdiction([listing.jurisdictions]); diff --git a/api/test/integration/application-flagged-set.e2e-spec.ts b/api/test/integration/application-flagged-set.e2e-spec.ts index 53193fdfdb..e8e7ffdc18 100644 --- a/api/test/integration/application-flagged-set.e2e-spec.ts +++ b/api/test/integration/application-flagged-set.e2e-spec.ts @@ -2019,14 +2019,13 @@ describe('Application flagged set Controller Tests', () => { `${listing}-email1@email.com-${listing}-firstname3-${listing}-lastname3-3-3-3-${listing}-firstname1-${listing}-lastname1-1-1-1`, ); expect(combinationAFS.applications).toHaveLength(6); - expect(combinationAFS.applications).toEqual([ - { id: app2.id }, - { id: app3.id }, - { id: app4.id }, - { id: app6.id }, - { id: app7.id }, - { id: app8.id }, - ]); + expect(combinationAFS.applications).toContainEqual({ id: app2.id }); + expect(combinationAFS.applications).toContainEqual({ id: app3.id }); + expect(combinationAFS.applications).toContainEqual({ id: app4.id }); + expect(combinationAFS.applications).toContainEqual({ id: app6.id }); + expect(combinationAFS.applications).toContainEqual({ id: app7.id }); + expect(combinationAFS.applications).toContainEqual({ id: app8.id }); + expect(combinationAFS.rule).toEqual(RuleEnum.combination); expect(combinationAFS.ruleKey).toEqual( `${listing}-email1@email.com-${listing}-firstname3-${listing}-lastname3-3-3-3-${listing}-firstname1-${listing}-lastname1-1-1-1`, diff --git a/api/test/integration/application.e2e-spec.ts b/api/test/integration/application.e2e-spec.ts index 56f2b62915..493bbdc532 100644 --- a/api/test/integration/application.e2e-spec.ts +++ b/api/test/integration/application.e2e-spec.ts @@ -1,3 +1,8 @@ +import cookieParser from 'cookie-parser'; +import { randomUUID } from 'crypto'; +import dayjs from 'dayjs'; +import { stringify } from 'qs'; +import request from 'supertest'; import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication, Logger } from '@nestjs/common'; import { @@ -13,39 +18,38 @@ import { UnitTypeEnum, YesNoEnum, } from '@prisma/client'; -import { randomUUID } from 'crypto'; -import { stringify } from 'qs'; -import request from 'supertest'; -import cookieParser from 'cookie-parser'; -import { AppModule } from '../../src/modules/app.module'; -import { PrismaService } from '../../src/services/prisma.service'; +import { addressFactory } from '../../prisma/seed-helpers/address-factory'; import { applicationFactory } from '../../prisma/seed-helpers/application-factory'; +import { createAllFeatureFlags } from '../../prisma/seed-helpers/feature-flag-factory'; +import { jurisdictionFactory } from '../../prisma/seed-helpers/jurisdiction-factory'; +import { listingFactory } from '../../prisma/seed-helpers/listing-factory'; +import { multiselectQuestionFactory } from '../../prisma/seed-helpers/multiselect-question-factory'; +import { reservedCommunityTypeFactoryAll } from '../../prisma/seed-helpers/reserved-community-type-factory'; +import { translationFactory } from '../../prisma/seed-helpers/translation-factory'; +import { userFactory } from '../../prisma/seed-helpers/user-factory'; import { unitTypeFactoryAll, unitTypeFactorySingle, } from '../../prisma/seed-helpers/unit-type-factory'; -import { ApplicationQueryParams } from '../../src/dtos/applications/application-query-params.dto'; -import { OrderByEnum } from '../../src/enums/shared/order-by-enum'; -import { ApplicationOrderByKeys } from '../../src/enums/applications/order-by-enum'; -import { listingFactory } from '../../prisma/seed-helpers/listing-factory'; -import { jurisdictionFactory } from '../../prisma/seed-helpers/jurisdiction-factory'; -import { ApplicationCreate } from '../../src/dtos/applications/application-create.dto'; -import { InputType } from '../../src/enums/shared/input-type-enum'; -import { addressFactory } from '../../prisma/seed-helpers/address-factory'; import { AddressCreate } from '../../src/dtos/addresses/address-create.dto'; +import { ApplicationCreate } from '../../src/dtos/applications/application-create.dto'; +import { ApplicationMultiselectQuestion } from '../../src/dtos/applications/application-multiselect-question.dto'; +import { ApplicationQueryParams } from '../../src/dtos/applications/application-query-params.dto'; +import { ApplicationSelectionCreate } from '../../src/dtos/applications/application-selection-create.dto'; import { ApplicationUpdate } from '../../src/dtos/applications/application-update.dto'; -import { translationFactory } from '../../prisma/seed-helpers/translation-factory'; -import { EmailService } from '../../src/services/email.service'; -import { userFactory } from '../../prisma/seed-helpers/user-factory'; +import { PublicAppsViewQueryParams } from '../../src/dtos/applications/public-apps-view-params.dto'; import { Login } from '../../src/dtos/auth/login.dto'; -import { multiselectQuestionFactory } from '../../prisma/seed-helpers/multiselect-question-factory'; -import { reservedCommunityTypeFactoryAll } from '../../prisma/seed-helpers/reserved-community-type-factory'; -import { ValidationMethod } from '../../src/enums/multiselect-questions/validation-method-enum'; import { AlternateContactRelationship } from '../../src/enums/applications/alternate-contact-relationship-enum'; -import { HouseholdMemberRelationship } from '../../src/enums/applications/household-member-relationship-enum'; +import { FeatureFlagEnum } from '../../src/enums/feature-flags/feature-flags-enum'; import { ApplicationsFilterEnum } from '../../src/enums/applications/filter-enum'; -import { PublicAppsViewQueryParams } from '../../src/dtos/applications/public-apps-view-params.dto'; -import dayjs from 'dayjs'; +import { HouseholdMemberRelationship } from '../../src/enums/applications/household-member-relationship-enum'; +import { ApplicationOrderByKeys } from '../../src/enums/applications/order-by-enum'; +import { ValidationMethod } from '../../src/enums/multiselect-questions/validation-method-enum'; +import { OrderByEnum } from '../../src/enums/shared/order-by-enum'; +import { InputType } from '../../src/enums/shared/input-type-enum'; +import { AppModule } from '../../src/modules/app.module'; +import { EmailService } from '../../src/services/email.service'; +import { PrismaService } from '../../src/services/prisma.service'; describe('Application Controller Tests', () => { let app: INestApplication; @@ -67,21 +71,127 @@ describe('Application Controller Tests', () => { jurisdictionId: string, listingId: string, section: MultiselectQuestionsApplicationSectionEnum, + version2 = false, ) => { - const res = await prisma.multiselectQuestions.create({ - data: multiselectQuestionFactory(jurisdictionId, { - multiselectQuestion: { - applicationSection: section, - listings: { - create: { - listingId: listingId, + return await prisma.multiselectQuestions.create({ + data: multiselectQuestionFactory( + jurisdictionId, + { + multiselectQuestion: { + applicationSection: section, + listings: { + create: { + listingId: listingId, + }, }, }, }, - }), + version2, + ), + include: { + multiselectOptions: true, + }, }); + }; - return res.id; + const applicationCreate = ( + exampleAddress: AddressCreate, + listingId: string, + submissionDate: Date, + unitTypeId: string, + applicationSelections?: ApplicationSelectionCreate[], + preferences?: ApplicationMultiselectQuestion[], + programs?: ApplicationMultiselectQuestion[], + submissionType: ApplicationSubmissionTypeEnum = ApplicationSubmissionTypeEnum.electronical, + ) => { + return { + acceptedTerms: true, + additionalPhone: true, + additionalPhoneNumber: '111-111-1111', + additionalPhoneNumberType: 'example type', + appUrl: 'http://www.example.com', + householdSize: 2, + housingStatus: 'example status', + householdExpectingChanges: false, + householdStudent: false, + incomeVouchers: false, + income: '36000', + incomePeriod: IncomePeriodEnum.perYear, + language: LanguagesEnum.en, + reviewStatus: ApplicationReviewStatusEnum.valid, + sendMailToMailingAddress: true, + status: ApplicationStatusEnum.submitted, + submissionDate: submissionDate, + submissionType: submissionType, + accessibility: { + mobility: false, + vision: false, + hearing: false, + }, + alternateContact: { + type: AlternateContactRelationship.friend, + otherType: 'example other type', + firstName: 'example first name', + lastName: 'example last name', + agency: 'example agency', + phoneNumber: '111-111-1111', + emailAddress: 'example@email.com', + address: exampleAddress, + }, + applicant: { + firstName: 'applicant first name', + middleName: 'applicant middle name', + lastName: 'applicant last name', + birthMonth: '12', + birthDay: '17', + birthYear: '1993', + emailAddress: 'example@email.com', + noEmail: false, + phoneNumber: '111-111-1111', + phoneNumberType: 'Cell', + noPhone: false, + workInRegion: YesNoEnum.yes, + applicantWorkAddress: exampleAddress, + applicantAddress: exampleAddress, + }, + applicationSelections: applicationSelections ?? [], + applicationsAlternateAddress: exampleAddress, + applicationsMailingAddress: exampleAddress, + contactPreferences: ['example contact preference'], + demographics: { + ethnicity: 'example ethnicity', + gender: 'example gender', + sexualOrientation: 'example sexual orientation', + howDidYouHear: ['example how did you hear'], + race: ['example race'], + }, + householdMember: [ + { + orderId: 0, + firstName: 'example first name', + middleName: 'example middle name', + lastName: 'example last name', + birthMonth: '12', + birthDay: '17', + birthYear: '1993', + sameAddress: YesNoEnum.yes, + relationship: HouseholdMemberRelationship.friend, + workInRegion: YesNoEnum.yes, + householdMemberWorkAddress: exampleAddress, + householdMemberAddress: exampleAddress, + }, + ], + listings: { + id: listingId, + }, + preferences: preferences ?? [], + preferredUnitTypes: [ + { + id: unitTypeId, + }, + ], + programs: programs ?? [], + }; }; beforeAll(async () => { @@ -103,6 +213,7 @@ describe('Application Controller Tests', () => { logger = moduleFixture.get(Logger); app.use(cookieParser()); await app.init(); + await createAllFeatureFlags(prisma); await unitTypeFactoryAll(prisma); await prisma.translations.create({ data: translationFactory(), @@ -252,7 +363,7 @@ describe('Application Controller Tests', () => { }); }); - describe('retreive endpoint', () => { + describe('retrieve endpoint', () => { it('should retrieve an application when one exists', async () => { const unitTypeA = await unitTypeFactorySingle( prisma, @@ -356,9 +467,13 @@ describe('Application Controller Tests', () => { }); }); - describe('submit endpoint', () => { + describe('submit endpoint with MSQ V1', () => { let publicUserCookies = ''; let storedUser = { id: '', email: '' }; + let unitTypeA; + let jurisdiction; + let listing1; + beforeAll(async () => { storedUser = await prisma.userAccounts.create({ data: await userFactory({ @@ -376,61 +491,131 @@ describe('Application Controller Tests', () => { .expect(201); publicUserCookies = resLogIn.headers['set-cookie']; - }); - it('should create application from public site', async () => { - const unitTypeA = await unitTypeFactorySingle( - prisma, - UnitTypeEnum.oneBdrm, - ); - const jurisdiction = await prisma.jurisdictions.create({ + unitTypeA = await unitTypeFactorySingle(prisma, UnitTypeEnum.oneBdrm); + jurisdiction = await prisma.jurisdictions.create({ data: jurisdictionFactory(), }); await reservedCommunityTypeFactoryAll(jurisdiction.id, prisma); - const listing1 = await listingFactory(jurisdiction.id, prisma, { - digitalApp: true, - }); - const listing1Created = await prisma.listings.create({ - data: listing1, + listing1 = await prisma.listings.create({ + data: await listingFactory(jurisdiction.id, prisma, { + digitalApp: true, + }), }); + }); - const multiselectQuestionProgram = await createMultiselectQuestion( - jurisdiction.id, - listing1Created.id, - MultiselectQuestionsApplicationSectionEnum.programs, - ); - const multiselectQuestionPreference = await createMultiselectQuestion( - jurisdiction.id, - listing1Created.id, - MultiselectQuestionsApplicationSectionEnum.preferences, - ); + it('should create application from public site', async () => { + const multiselectQuestionPreferenceId = ( + await createMultiselectQuestion( + jurisdiction.id, + listing1.id, + MultiselectQuestionsApplicationSectionEnum.preferences, + ) + )?.id; + const multiselectQuestionProgramId = ( + await createMultiselectQuestion( + jurisdiction.id, + listing1.id, + MultiselectQuestionsApplicationSectionEnum.programs, + ) + ).id; - const submissionDate = new Date(); const exampleAddress = addressFactory() as AddressCreate; - const dto: ApplicationCreate = { - contactPreferences: ['example contact preference'], - preferences: [ - { - multiselectQuestionId: multiselectQuestionPreference, - key: 'example key', - claimed: true, - options: [ - { - key: 'example key', - checked: true, - extraData: [ - { - type: InputType.boolean, - key: 'example key', - value: true, - }, - ], - }, - ], + const preferences = [ + { + multiselectQuestionId: multiselectQuestionPreferenceId, + key: 'example key', + claimed: true, + options: [ + { + key: 'example key', + checked: true, + extraData: [ + { + type: InputType.boolean, + key: 'example key', + value: true, + }, + ], + }, + ], + }, + ]; + + const programs = [ + { + multiselectQuestionId: multiselectQuestionProgramId, + key: 'example key', + claimed: true, + options: [ + { + key: 'example key', + checked: true, + extraData: [ + { + type: InputType.boolean, + key: 'example key', + value: true, + }, + ], + }, + ], + }, + ]; + const submissionDate = new Date(); + const dto: ApplicationCreate = applicationCreate( + exampleAddress, + listing1.id, + submissionDate, + unitTypeA.id, + [], + preferences, + programs, + ); + const res = await request(app.getHttpServer()) + .post(`/applications/submit`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .send(dto) + .set('Cookie', publicUserCookies) + .expect(201); + + expect(res.body.id).not.toBeNull(); + expect(res.body).toEqual({ + ...dto, + id: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String), + deletedAt: null, + confirmationCode: expect.any(String), + accessibleUnitWaitlistNumber: null, + conventionalUnitWaitlistNumber: null, + isNewest: true, + markedAsDuplicate: false, + manualLotteryPositionNumber: null, + submissionDate: expect.any(String), + accessibility: { + id: expect.any(String), + mobility: false, + vision: false, + hearing: false, + other: null, + }, + alternateContact: { + id: expect.any(String), + type: 'friend', + otherType: 'example other type', + firstName: 'example first name', + lastName: 'example last name', + agency: 'example agency', + phoneNumber: '111-111-1111', + emailAddress: 'example@email.com', + address: { + ...exampleAddress, + id: expect.any(String), + street2: null, }, - ], - status: ApplicationStatusEnum.submitted, - submissionType: ApplicationSubmissionTypeEnum.electronical, + }, applicant: { + id: expect.any(String), firstName: 'applicant first name', middleName: 'applicant middle name', lastName: 'applicant last name', @@ -442,95 +627,375 @@ describe('Application Controller Tests', () => { phoneNumber: '111-111-1111', phoneNumberType: 'Cell', noPhone: false, - workInRegion: YesNoEnum.yes, - applicantWorkAddress: exampleAddress, - applicantAddress: exampleAddress, - }, - accessibility: { - mobility: false, - vision: false, - hearing: false, + workInRegion: 'yes', + fullTimeStudent: null, + applicantWorkAddress: { + ...exampleAddress, + id: expect.any(String), + street2: null, + }, + applicantAddress: { + ...exampleAddress, + id: expect.any(String), + street2: null, + }, }, - alternateContact: { - type: AlternateContactRelationship.friend, - otherType: 'example other type', - firstName: 'example first name', - lastName: 'example last name', - agency: 'example agency', - phoneNumber: '111-111-1111', - emailAddress: 'example@email.com', - address: exampleAddress, + applicationSelections: [], + applicationsAlternateAddress: { + ...exampleAddress, + id: expect.any(String), + street2: null, }, - applicationsAlternateAddress: exampleAddress, - applicationsMailingAddress: exampleAddress, - listings: { - id: listing1Created.id, + applicationsMailingAddress: { + ...exampleAddress, + id: expect.any(String), + street2: null, }, demographics: { + id: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String), ethnicity: 'example ethnicity', gender: 'example gender', sexualOrientation: 'example sexual orientation', howDidYouHear: ['example how did you hear'], race: ['example race'], }, - preferredUnitTypes: [ - { - id: unitTypeA.id, - }, - ], householdMember: [ { - orderId: 0, - firstName: 'example first name', - middleName: 'example middle name', - lastName: 'example last name', - birthMonth: '12', birthDay: '17', + birthMonth: '12', birthYear: '1993', - sameAddress: YesNoEnum.yes, - relationship: HouseholdMemberRelationship.friend, - workInRegion: YesNoEnum.yes, - householdMemberWorkAddress: exampleAddress, - householdMemberAddress: exampleAddress, + firstName: 'example first name', + fullTimeStudent: null, + householdMemberAddress: { + ...exampleAddress, + id: expect.any(String), + street2: null, + }, + householdMemberWorkAddress: { + ...exampleAddress, + id: expect.any(String), + street2: null, + }, + id: expect.any(String), + lastName: 'example last name', + middleName: 'example middle name', + orderId: 0, + relationship: 'friend', + sameAddress: 'yes', + workInRegion: 'yes', }, ], - appUrl: 'http://www.example.com', - additionalPhone: true, - additionalPhoneNumber: '111-111-1111', - additionalPhoneNumberType: 'example type', - householdSize: 2, - housingStatus: 'example status', - sendMailToMailingAddress: true, - householdExpectingChanges: false, - householdStudent: false, - incomeVouchers: false, - income: '36000', - incomePeriod: IncomePeriodEnum.perYear, - language: LanguagesEnum.en, - acceptedTerms: true, - submissionDate: submissionDate, - reviewStatus: ApplicationReviewStatusEnum.valid, - programs: [ + listings: { + id: listing1.id, + name: listing1.name, + }, + preferences: [ { - multiselectQuestionId: multiselectQuestionProgram, - key: 'example key', claimed: true, + key: 'example key', + multiselectQuestionId: expect.any(String), options: [ { + checked: true, + extraData: [ + { + key: 'example key', + type: 'boolean', + value: true, + }, + ], key: 'example key', + }, + ], + }, + ], + preferredUnitTypes: [ + { + id: unitTypeA.id, + name: 'oneBdrm', + numBedrooms: 1, + }, + ], + programs: [ + { + claimed: true, + key: 'example key', + multiselectQuestionId: expect.any(String), + options: [ + { checked: true, extraData: [ { - type: InputType.boolean, key: 'example key', + type: 'boolean', value: true, }, ], + key: 'example key', }, ], }, ], - }; + }); + expect(mockApplicationConfirmation).toBeCalledTimes(1); + }); + + it('should throw an error when submitting an application from the public site on a listing with no common app', async () => { + const listingNoDigitalApp = await prisma.listings.create({ + data: await listingFactory(jurisdiction.id, prisma, { + digitalApp: false, + }), + }); + + const exampleAddress = addressFactory() as AddressCreate; + const submissionDate = new Date(); + + const dto: ApplicationCreate = applicationCreate( + exampleAddress, + listingNoDigitalApp.id, + submissionDate, + unitTypeA.id, + ); + const res = await request(app.getHttpServer()) + .post(`/applications/submit`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .send(dto) + .set('Cookie', publicUserCookies) + .expect(400); + expect(res.body.message).toEqual( + `Listing is not open for application submission`, + ); + }); + + it('should set the isNewest flag', async () => { + const listing2 = await prisma.listings.create({ + data: await listingFactory(jurisdiction.id, prisma, { + digitalApp: true, + }), + }); + + // create previous applications for the user with one being the newest + await prisma.applications.create({ + data: { + listings: { connect: { id: listing2.id } }, + confirmationCode: randomUUID(), + preferences: [], + submissionType: ApplicationSubmissionTypeEnum.electronical, + status: ApplicationStatusEnum.submitted, + isNewest: true, + }, + }); + + await prisma.applications.create({ + data: { + listings: { connect: { id: listing2.id } }, + confirmationCode: randomUUID(), + preferences: [], + submissionType: ApplicationSubmissionTypeEnum.electronical, + status: ApplicationStatusEnum.submitted, + isNewest: false, + }, + }); + + const exampleAddress = addressFactory() as AddressCreate; + const submissionDate = new Date(); + + const dto: ApplicationCreate = applicationCreate( + exampleAddress, + listing1.id, + submissionDate, + unitTypeA.id, + ); + const res = await request(app.getHttpServer()) + .post(`/applications/submit`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .send(dto) + .set('Cookie', publicUserCookies) + .expect(201); + + expect(res.body.id).not.toBeNull(); + expect(res.body.isNewest).toBe(true); + expect(mockApplicationConfirmation).toBeCalledTimes(1); + + const otherUserApplications = await prisma.applications.findMany({ + select: { id: true, isNewest: true }, + where: { userId: storedUser.id, id: { not: res.body.id } }, + }); + otherUserApplications.forEach((application) => { + expect(application.isNewest).toBe(false); + }); + }); + + it('should calculate geocoding on application', async () => { + const exampleAddress = addressFactory() as AddressCreate; + + const listingGeocoding = await prisma.listings.create({ + data: await listingFactory(jurisdiction.id, prisma, { + digitalApp: true, + listing: { + listingsBuildingAddress: { create: exampleAddress }, + } as unknown as Prisma.ListingsCreateInput, + }), + }); + const multiselectQuestionPreference = + await prisma.multiselectQuestions.create({ + data: multiselectQuestionFactory(jurisdiction.id, { + multiselectQuestion: { + applicationSection: + MultiselectQuestionsApplicationSectionEnum.preferences, + listings: { + create: { + listingId: listingGeocoding.id, + }, + }, + options: [ + { + text: 'geocoding preference', + collectAddress: true, + validationMethod: ValidationMethod.radius, + radiusSize: 5, + }, + ], + }, + }), + }); + + const preferences = [ + { + multiselectQuestionId: multiselectQuestionPreference.id, + key: multiselectQuestionPreference.text, + claimed: true, + options: [ + { + key: 'geocoding preference', + checked: true, + extraData: [ + { + type: InputType.address, + key: 'address', + value: exampleAddress, + }, + ], + }, + ], + }, + ]; + const submissionDate = new Date(); + + const dto: ApplicationCreate = applicationCreate( + exampleAddress, + listingGeocoding.id, + submissionDate, + unitTypeA.id, + [], + preferences, + ); + const res = await request(app.getHttpServer()) + .post(`/applications/submit`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .send(dto) + .expect(201); + + expect(res.body.id).not.toBeNull(); + + let savedApplication = await prisma.applications.findMany({ + where: { + id: res.body.id, + }, + }); + const savedPreferences = savedApplication[0].preferences; + expect(savedPreferences).toHaveLength(1); + let geocodingOptions = savedPreferences[0].options[0]; + // This catches the edge case where the geocoding hasn't completed yet + if (geocodingOptions.extraData.length === 1) { + // I'm unsure why removing this console log makes this test fail. This should be looked into + console.log(''); + savedApplication = await prisma.applications.findMany({ + where: { + id: res.body.id, + }, + }); + } + geocodingOptions = savedApplication[0].preferences[0].options[0]; + expect(geocodingOptions.extraData).toHaveLength(2); + expect(geocodingOptions.extraData).toContainEqual({ + key: 'geocodingVerified', + type: 'text', + value: true, + }); + }); + }); + + describe('submit endpoint with MSQ V2', () => { + let publicUserCookies = ''; + let storedUser = { id: '', email: '' }; + let unitTypeA; + let jurisdiction; + let listing1; + + beforeAll(async () => { + storedUser = await prisma.userAccounts.create({ + data: await userFactory({ + mfaEnabled: false, + confirmedAt: new Date(), + }), + }); + const resLogIn = await request(app.getHttpServer()) + .post('/auth/login') + .set({ passkey: process.env.API_PASS_KEY || '' }) + .send({ + email: storedUser.email, + password: 'Abcdef12345!', + } as Login) + .expect(201); + + publicUserCookies = resLogIn.headers['set-cookie']; + unitTypeA = await unitTypeFactorySingle(prisma, UnitTypeEnum.oneBdrm); + jurisdiction = await prisma.jurisdictions.create({ + data: jurisdictionFactory('applicationSubmitWithV2MSQ', { + featureFlags: [FeatureFlagEnum.enableV2MSQ], + }), + }); + await reservedCommunityTypeFactoryAll(jurisdiction.id, prisma); + listing1 = await prisma.listings.create({ + data: await listingFactory(jurisdiction.id, prisma, { + digitalApp: true, + }), + }); + }); + + it('should create application from public site', async () => { + const multiselectQuestion = await createMultiselectQuestion( + jurisdiction.id, + listing1.id, + MultiselectQuestionsApplicationSectionEnum.preferences, + true, + ); + const exampleAddress = addressFactory() as AddressCreate; + const submissionDate = new Date(); + const applicationSelections = [ + { + multiselectQuestion: { id: multiselectQuestion.id }, + selections: [ + { + addressHolderAddress: exampleAddress, + multiselectOption: { + id: multiselectQuestion.multiselectOptions[0].id, + }, + }, + ], + }, + ]; + + const dto: ApplicationCreate = applicationCreate( + exampleAddress, + listing1.id, + submissionDate, + unitTypeA.id, + applicationSelections, + ); const res = await request(app.getHttpServer()) .post(`/applications/submit`) .set({ passkey: process.env.API_PASS_KEY || '' }) @@ -540,58 +1005,18 @@ describe('Application Controller Tests', () => { expect(res.body.id).not.toBeNull(); expect(res.body).toEqual({ + ...dto, id: expect.any(String), createdAt: expect.any(String), updatedAt: expect.any(String), deletedAt: null, - appUrl: 'http://www.example.com', - additionalPhone: true, - additionalPhoneNumber: '111-111-1111', - additionalPhoneNumberType: 'example type', - contactPreferences: ['example contact preference'], - householdSize: 2, - housingStatus: 'example status', - sendMailToMailingAddress: true, - householdExpectingChanges: false, - householdStudent: false, - incomeVouchers: false, - income: '36000', - incomePeriod: 'perYear', - status: 'submitted', - language: 'en', - acceptedTerms: true, - submissionType: 'electronical', - submissionDate: expect.any(String), - markedAsDuplicate: false, confirmationCode: expect.any(String), accessibleUnitWaitlistNumber: null, conventionalUnitWaitlistNumber: null, + isNewest: true, + markedAsDuplicate: false, manualLotteryPositionNumber: null, - reviewStatus: 'valid', - applicationsMailingAddress: { - id: expect.any(String), - placeName: exampleAddress.placeName, - city: exampleAddress.city, - county: exampleAddress.county, - state: exampleAddress.state, - street: exampleAddress.street, - street2: null, - zipCode: exampleAddress.zipCode, - latitude: exampleAddress.latitude, - longitude: exampleAddress.longitude, - }, - applicationsAlternateAddress: { - id: expect.any(String), - placeName: exampleAddress.placeName, - city: exampleAddress.city, - county: exampleAddress.county, - state: exampleAddress.state, - street: exampleAddress.street, - street2: null, - zipCode: exampleAddress.zipCode, - latitude: exampleAddress.latitude, - longitude: exampleAddress.longitude, - }, + submissionDate: expect.any(String), accessibility: { id: expect.any(String), mobility: false, @@ -599,23 +1024,21 @@ describe('Application Controller Tests', () => { hearing: false, other: null, }, - demographics: { + alternateContact: { id: expect.any(String), - createdAt: expect.any(String), - updatedAt: expect.any(String), - ethnicity: 'example ethnicity', - gender: 'example gender', - sexualOrientation: 'example sexual orientation', - howDidYouHear: ['example how did you hear'], - race: ['example race'], - }, - preferredUnitTypes: [ - { - id: unitTypeA.id, - name: 'oneBdrm', - numBedrooms: 1, + type: 'friend', + otherType: 'example other type', + firstName: 'example first name', + lastName: 'example last name', + agency: 'example agency', + phoneNumber: '111-111-1111', + emailAddress: 'example@email.com', + address: { + ...exampleAddress, + id: expect.any(String), + street2: null, }, - ], + }, applicant: { id: expect.any(String), firstName: 'applicant first name', @@ -632,295 +1055,126 @@ describe('Application Controller Tests', () => { workInRegion: 'yes', fullTimeStudent: null, applicantWorkAddress: { + ...exampleAddress, id: expect.any(String), - placeName: exampleAddress.placeName, - city: exampleAddress.city, - county: exampleAddress.county, - state: exampleAddress.state, - street: exampleAddress.street, street2: null, - zipCode: exampleAddress.zipCode, - latitude: exampleAddress.latitude, - longitude: exampleAddress.longitude, }, applicantAddress: { + ...exampleAddress, id: expect.any(String), - placeName: exampleAddress.placeName, - city: exampleAddress.city, - county: exampleAddress.county, - state: exampleAddress.state, - street: exampleAddress.street, street2: null, - zipCode: exampleAddress.zipCode, - latitude: exampleAddress.latitude, - longitude: exampleAddress.longitude, }, }, - alternateContact: { - id: expect.any(String), - type: 'friend', - otherType: 'example other type', - firstName: 'example first name', - lastName: 'example last name', - agency: 'example agency', - phoneNumber: '111-111-1111', - emailAddress: 'example@email.com', - address: { - id: expect.any(String), - placeName: exampleAddress.placeName, - city: exampleAddress.city, - county: exampleAddress.county, - state: exampleAddress.state, - street: exampleAddress.street, - street2: null, - zipCode: exampleAddress.zipCode, - latitude: exampleAddress.latitude, - longitude: exampleAddress.longitude, - }, - }, - householdMember: [ + applicationSelections: [ { - birthDay: '17', - birthMonth: '12', - birthYear: '1993', - firstName: 'example first name', - fullTimeStudent: null, - householdMemberAddress: { - id: expect.any(String), - placeName: exampleAddress.placeName, - city: exampleAddress.city, - county: exampleAddress.county, - state: exampleAddress.state, - street: exampleAddress.street, - street2: null, - zipCode: exampleAddress.zipCode, - latitude: exampleAddress.latitude, - longitude: exampleAddress.longitude, - }, - householdMemberWorkAddress: { - id: expect.any(String), - placeName: exampleAddress.placeName, - city: exampleAddress.city, - county: exampleAddress.county, - state: exampleAddress.state, - street: exampleAddress.street, - street2: null, - zipCode: exampleAddress.zipCode, - latitude: exampleAddress.latitude, - longitude: exampleAddress.longitude, - }, id: expect.any(String), - lastName: 'example last name', - middleName: 'example middle name', - orderId: 0, - relationship: 'friend', - sameAddress: 'yes', - workInRegion: 'yes', - }, - ], - preferences: [ - { - claimed: true, - key: 'example key', - multiselectQuestionId: expect.any(String), - options: [ - { - checked: true, - extraData: [ - { - key: 'example key', - type: 'boolean', - value: true, - }, - ], - key: 'example key', - }, - ], - }, - ], - programs: [ - { - claimed: true, - key: 'example key', - multiselectQuestionId: expect.any(String), - options: [ - { - checked: true, - extraData: [ - { - key: 'example key', - type: 'boolean', - value: true, - }, - ], - key: 'example key', - }, - ], - }, - ], - listings: { - id: listing1Created.id, - name: listing1.name, - }, - isNewest: true, - }); - expect(mockApplicationConfirmation).toBeCalledTimes(1); - }); - - it('should throw an error when submitting an application from the public site on a listing with no common app', 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, { - digitalApp: false, - }); - const listing1Created = await prisma.listings.create({ - data: listing1, - }); - - const multiselectQuestionProgram = await createMultiselectQuestion( - jurisdiction.id, - listing1Created.id, - MultiselectQuestionsApplicationSectionEnum.programs, - ); - const multiselectQuestionPreference = await createMultiselectQuestion( - jurisdiction.id, - listing1Created.id, - MultiselectQuestionsApplicationSectionEnum.preferences, - ); - - const submissionDate = new Date(); - const exampleAddress = addressFactory() as AddressCreate; - const dto: ApplicationCreate = { - contactPreferences: ['example contact preference'], - preferences: [ - { - multiselectQuestionId: multiselectQuestionPreference, - key: 'example key', - claimed: true, - options: [ + createdAt: expect.any(String), + updatedAt: expect.any(String), + hasOptedOut: false, + multiselectQuestion: { + id: multiselectQuestion.id, + name: multiselectQuestion.name, + }, + selections: [ { - key: 'example key', - checked: true, - extraData: [ - { - type: InputType.boolean, - key: 'example key', - value: true, - }, - ], + id: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String), + addressHolderAddress: { + ...exampleAddress, + id: expect.any(String), + street2: null, + }, + addressHolderName: null, + addressHolderRelationship: null, + isGeocodingVerified: null, + multiselectOption: { + id: multiselectQuestion.multiselectOptions[0].id, + name: multiselectQuestion.multiselectOptions[0].name, + ordinal: multiselectQuestion.multiselectOptions[0].ordinal, + }, }, ], }, ], - status: ApplicationStatusEnum.submitted, - submissionType: ApplicationSubmissionTypeEnum.electronical, - applicant: { - firstName: 'applicant first name', - middleName: 'applicant middle name', - lastName: 'applicant last name', - birthMonth: '12', - birthDay: '17', - birthYear: '1993', - emailAddress: 'example@email.com', - noEmail: false, - phoneNumber: '111-111-1111', - phoneNumberType: 'Cell', - noPhone: false, - workInRegion: YesNoEnum.yes, - applicantWorkAddress: exampleAddress, - applicantAddress: exampleAddress, - }, - accessibility: { - mobility: false, - vision: false, - hearing: false, - }, - alternateContact: { - type: AlternateContactRelationship.friend, - otherType: 'example other type', - firstName: 'example first name', - lastName: 'example last name', - agency: 'example agency', - phoneNumber: '111-111-1111', - emailAddress: 'example@email.com', - address: exampleAddress, + applicationsAlternateAddress: { + ...exampleAddress, + id: expect.any(String), + street2: null, }, - applicationsAlternateAddress: exampleAddress, - applicationsMailingAddress: exampleAddress, - listings: { - id: listing1Created.id, + applicationsMailingAddress: { + ...exampleAddress, + id: expect.any(String), + street2: null, }, demographics: { + id: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String), ethnicity: 'example ethnicity', gender: 'example gender', sexualOrientation: 'example sexual orientation', howDidYouHear: ['example how did you hear'], race: ['example race'], }, - preferredUnitTypes: [ - { - id: unitTypeA.id, - }, - ], householdMember: [ { - orderId: 0, + birthDay: '17', + birthMonth: '12', + birthYear: '1993', firstName: 'example first name', - middleName: 'example middle name', + fullTimeStudent: null, + householdMemberAddress: { + ...exampleAddress, + id: expect.any(String), + street2: null, + }, + householdMemberWorkAddress: { + ...exampleAddress, + id: expect.any(String), + street2: null, + }, + id: expect.any(String), lastName: 'example last name', - birthMonth: '12', - birthDay: '17', - birthYear: '1993', - sameAddress: YesNoEnum.yes, - relationship: HouseholdMemberRelationship.friend, - workInRegion: YesNoEnum.yes, - householdMemberWorkAddress: exampleAddress, - householdMemberAddress: exampleAddress, + middleName: 'example middle name', + orderId: 0, + relationship: 'friend', + sameAddress: 'yes', + workInRegion: 'yes', }, ], - appUrl: 'http://www.example.com', - additionalPhone: true, - additionalPhoneNumber: '111-111-1111', - additionalPhoneNumberType: 'example type', - householdSize: 2, - housingStatus: 'example status', - sendMailToMailingAddress: true, - householdExpectingChanges: false, - householdStudent: false, - incomeVouchers: false, - income: '36000', - incomePeriod: IncomePeriodEnum.perYear, - language: LanguagesEnum.en, - acceptedTerms: true, - submissionDate: submissionDate, - reviewStatus: ApplicationReviewStatusEnum.valid, - programs: [ + listings: { + id: listing1.id, + name: listing1.name, + }, + preferences: [], + preferredUnitTypes: [ { - multiselectQuestionId: multiselectQuestionProgram, - key: 'example key', - claimed: true, - options: [ - { - key: 'example key', - checked: true, - extraData: [ - { - type: InputType.boolean, - key: 'example key', - value: true, - }, - ], - }, - ], + id: unitTypeA.id, + name: 'oneBdrm', + numBedrooms: 1, }, ], - }; + programs: [], + }); + expect(mockApplicationConfirmation).toBeCalledTimes(1); + }); + + it('should throw an error when submitting an application from the public site on a listing with no common app', async () => { + const listingNoDigitalApp = await prisma.listings.create({ + data: await listingFactory(jurisdiction.id, prisma, { + digitalApp: false, + }), + }); + + const submissionDate = new Date(); + const exampleAddress = addressFactory() as AddressCreate; + const dto: ApplicationCreate = applicationCreate( + exampleAddress, + listingNoDigitalApp.id, + submissionDate, + unitTypeA.id, + ); const res = await request(app.getHttpServer()) .post(`/applications/submit`) .set({ passkey: process.env.API_PASS_KEY || '' }) @@ -933,32 +1187,16 @@ describe('Application Controller Tests', () => { }); it('should set the isNewest flag', 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, { - digitalApp: true, - }); - const listing1Created = await prisma.listings.create({ - data: listing1, - }); - - const listing2 = await listingFactory(jurisdiction.id, prisma, { - digitalApp: true, - }); - const listing2Created = await prisma.listings.create({ - data: listing2, + const listing2 = await prisma.listings.create({ + data: await listingFactory(jurisdiction.id, prisma, { + digitalApp: true, + }), }); // create previous applications for the user with one being the newest await prisma.applications.create({ data: { - listings: { connect: { id: listing2Created.id } }, + listings: { connect: { id: listing2.id } }, confirmationCode: randomUUID(), preferences: [], submissionType: ApplicationSubmissionTypeEnum.electronical, @@ -969,7 +1207,7 @@ describe('Application Controller Tests', () => { await prisma.applications.create({ data: { - listings: { connect: { id: listing2Created.id } }, + listings: { connect: { id: listing2.id } }, confirmationCode: randomUUID(), preferences: [], submissionType: ApplicationSubmissionTypeEnum.electronical, @@ -980,78 +1218,12 @@ describe('Application Controller Tests', () => { const submissionDate = new Date(); const exampleAddress = addressFactory() as AddressCreate; - const dto: ApplicationCreate = { - contactPreferences: ['example contact preference'], - preferences: [], - status: ApplicationStatusEnum.submitted, - submissionType: ApplicationSubmissionTypeEnum.electronical, - applicant: { - firstName: 'applicant first name', - middleName: 'applicant middle name', - lastName: 'applicant last name', - birthMonth: '12', - birthDay: '17', - birthYear: '1993', - emailAddress: 'example@email.com', - noEmail: false, - phoneNumber: '111-111-1111', - phoneNumberType: 'Cell', - noPhone: false, - workInRegion: YesNoEnum.yes, - applicantWorkAddress: exampleAddress, - applicantAddress: exampleAddress, - }, - accessibility: { - mobility: false, - vision: false, - hearing: false, - }, - alternateContact: { - type: AlternateContactRelationship.friend, - otherType: 'example other type', - firstName: 'example first name', - lastName: 'example last name', - agency: 'example agency', - phoneNumber: '111-111-1111', - emailAddress: 'example@email.com', - address: exampleAddress, - }, - applicationsAlternateAddress: exampleAddress, - applicationsMailingAddress: exampleAddress, - listings: { - id: listing1Created.id, - }, - demographics: { - ethnicity: 'example ethnicity', - gender: 'example gender', - sexualOrientation: 'example sexual orientation', - howDidYouHear: ['example how did you hear'], - race: ['example race'], - }, - preferredUnitTypes: [ - { - id: unitTypeA.id, - }, - ], - householdMember: [], - appUrl: 'http://www.example.com', - additionalPhone: true, - additionalPhoneNumber: '111-111-1111', - additionalPhoneNumberType: 'example type', - householdSize: 2, - housingStatus: 'example status', - sendMailToMailingAddress: true, - householdExpectingChanges: false, - householdStudent: false, - incomeVouchers: false, - income: '36000', - incomePeriod: IncomePeriodEnum.perYear, - language: LanguagesEnum.en, - acceptedTerms: true, - submissionDate: submissionDate, - reviewStatus: ApplicationReviewStatusEnum.valid, - programs: [], - }; + const dto: ApplicationCreate = applicationCreate( + exampleAddress, + listing1.id, + submissionDate, + unitTypeA.id, + ); const res = await request(app.getHttpServer()) .post(`/applications/submit`) .set({ passkey: process.env.API_PASS_KEY || '' }) @@ -1072,156 +1244,69 @@ describe('Application Controller Tests', () => { }); }); - it('should calculate geocoding on application', async () => { - const unitTypeA = await unitTypeFactorySingle( - prisma, - UnitTypeEnum.oneBdrm, - ); - const jurisdiction = await prisma.jurisdictions.create({ - data: jurisdictionFactory(), - }); - await reservedCommunityTypeFactoryAll(jurisdiction.id, prisma); + it.skip('should calculate geocoding on application', async () => { const exampleAddress = addressFactory() as AddressCreate; - const listing1 = await listingFactory(jurisdiction.id, prisma, { - digitalApp: true, - listing: { - listingsBuildingAddress: { create: exampleAddress }, - } as unknown as Prisma.ListingsCreateInput, - }); - const listing1Created = await prisma.listings.create({ - data: listing1, - }); - - const multiselectQuestionPreference = - await prisma.multiselectQuestions.create({ - data: multiselectQuestionFactory(jurisdiction.id, { - multiselectQuestion: { - applicationSection: - MultiselectQuestionsApplicationSectionEnum.preferences, - listings: { - create: { - listingId: listing1Created.id, - }, - }, - options: [ - { - text: 'geocoding preference', - collectAddress: true, - validationMethod: ValidationMethod.radius, - radiusSize: 5, - }, - ], - }, - }), - }); - - const submissionDate = new Date(); - const dto: ApplicationCreate = { - contactPreferences: ['example contact preference'], - preferences: [ - { - multiselectQuestionId: multiselectQuestionPreference.id, - key: multiselectQuestionPreference.text, - claimed: true, - options: [ - { - key: 'geocoding preference', - checked: true, - extraData: [ - { - type: InputType.address, - key: 'address', - value: exampleAddress, - }, - ], - }, - ], - }, - ], - status: ApplicationStatusEnum.submitted, - submissionType: ApplicationSubmissionTypeEnum.electronical, - applicant: { - firstName: 'applicant first name', - middleName: 'applicant middle name', - lastName: 'applicant last name', - birthMonth: '12', - birthDay: '17', - birthYear: '1993', - emailAddress: 'example@email.com', - noEmail: false, - phoneNumber: '111-111-1111', - phoneNumberType: 'Cell', - noPhone: false, - workInRegion: YesNoEnum.yes, - applicantWorkAddress: exampleAddress, - applicantAddress: exampleAddress, - }, - accessibility: { - mobility: false, - vision: false, - hearing: false, - }, - alternateContact: { - type: AlternateContactRelationship.friend, - otherType: 'example other type', - firstName: 'example first name', - lastName: 'example last name', - agency: 'example agency', - phoneNumber: '111-111-1111', - emailAddress: 'example@email.com', - address: exampleAddress, - }, - applicationsAlternateAddress: exampleAddress, - applicationsMailingAddress: exampleAddress, - listings: { - id: listing1Created.id, - }, - demographics: { - ethnicity: 'example ethnicity', - gender: 'example gender', - sexualOrientation: 'example sexual orientation', - howDidYouHear: ['example how did you hear'], - race: ['example race'], + const listingGeocoding = await prisma.listings.create({ + data: await listingFactory(jurisdiction.id, prisma, { + digitalApp: true, + listing: { + listingsBuildingAddress: { create: exampleAddress }, + } as unknown as Prisma.ListingsCreateInput, + }), + }); + + const multiselectQuestionPreference = + await prisma.multiselectQuestions.create({ + data: multiselectQuestionFactory(jurisdiction.id, { + multiselectQuestion: { + applicationSection: + MultiselectQuestionsApplicationSectionEnum.preferences, + listings: { + create: { + listingId: listingGeocoding.id, + }, + }, + options: [ + { + text: 'geocoding preference', + collectAddress: true, + validationMethod: ValidationMethod.radius, + radiusSize: 5, + }, + ], + }, + }), + }); + const preferences = [ + { + multiselectQuestionId: multiselectQuestionPreference.id, + key: multiselectQuestionPreference.text, + claimed: true, + options: [ + { + key: 'geocoding preference', + checked: true, + extraData: [ + { + type: InputType.address, + key: 'address', + value: exampleAddress, + }, + ], + }, + ], }, - preferredUnitTypes: [ - { - id: unitTypeA.id, - }, - ], - householdMember: [ - { - orderId: 0, - firstName: 'example first name', - middleName: 'example middle name', - lastName: 'example last name', - birthMonth: '12', - birthDay: '17', - birthYear: '1993', - sameAddress: YesNoEnum.yes, - relationship: HouseholdMemberRelationship.friend, - workInRegion: YesNoEnum.yes, - householdMemberWorkAddress: exampleAddress, - householdMemberAddress: exampleAddress, - }, - ], - appUrl: 'http://www.example.com', - additionalPhone: true, - additionalPhoneNumber: '111-111-1111', - additionalPhoneNumberType: 'example type', - householdSize: 2, - housingStatus: 'example status', - sendMailToMailingAddress: true, - householdExpectingChanges: false, - householdStudent: false, - incomeVouchers: false, - income: '36000', - incomePeriod: IncomePeriodEnum.perYear, - language: LanguagesEnum.en, - acceptedTerms: true, - submissionDate: submissionDate, - reviewStatus: ApplicationReviewStatusEnum.valid, - programs: [], - }; + ]; + + const submissionDate = new Date(); + const dto: ApplicationCreate = applicationCreate( + exampleAddress, + listingGeocoding.id, + submissionDate, + unitTypeA.id, + [], + preferences, + ); const res = await request(app.getHttpServer()) .post(`/applications/submit`) .set({ passkey: process.env.API_PASS_KEY || '' }) @@ -1259,7 +1344,7 @@ describe('Application Controller Tests', () => { }); describe('create endpoint', () => { - it('should create application from partner site', async () => { + it('should create application from partner site with MSQ V1', async () => { const unitTypeA = await unitTypeFactorySingle( prisma, UnitTypeEnum.oneBdrm, @@ -1268,149 +1353,133 @@ describe('Application Controller Tests', () => { data: jurisdictionFactory(), }); await reservedCommunityTypeFactoryAll(jurisdiction.id, prisma); - const listing1 = await listingFactory(jurisdiction.id, prisma); - const listing1Created = await prisma.listings.create({ - data: listing1, - }); + const listing1 = await prisma.listings.create({ + data: await listingFactory(jurisdiction.id, prisma), + }); + const multiselectQuestionPreferenceId = ( + await createMultiselectQuestion( + jurisdiction.id, + listing1.id, + MultiselectQuestionsApplicationSectionEnum.preferences, + ) + ).id; + const multiselectQuestionProgramId = ( + await createMultiselectQuestion( + jurisdiction.id, + listing1.id, + MultiselectQuestionsApplicationSectionEnum.programs, + ) + ).id; - const multiselectQuestionProgram = await createMultiselectQuestion( - jurisdiction.id, - listing1Created.id, - MultiselectQuestionsApplicationSectionEnum.programs, + const exampleAddress = addressFactory() as AddressCreate; + const preferences = [ + { + multiselectQuestionId: multiselectQuestionPreferenceId, + key: 'example key', + claimed: true, + options: [ + { + key: 'example key', + checked: true, + extraData: [ + { + type: InputType.boolean, + key: 'example key', + value: true, + }, + ], + }, + ], + }, + ]; + const programs = [ + { + multiselectQuestionId: multiselectQuestionProgramId, + key: 'example key', + claimed: true, + options: [ + { + key: 'example key', + checked: true, + extraData: [ + { + type: InputType.boolean, + key: 'example key', + value: true, + }, + ], + }, + ], + }, + ]; + const submissionDate = new Date(); + + const dto: ApplicationCreate = applicationCreate( + exampleAddress, + listing1.id, + submissionDate, + unitTypeA.id, + [], + preferences, + programs, + ApplicationSubmissionTypeEnum.paper, + ); + const res = await request(app.getHttpServer()) + .post(`/applications/`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .send(dto) + .set('Cookie', adminCookies) + .expect(201); + + expect(res.body.id).not.toBeNull(); + }); + it('should create application from partner site with MSQ V2', async () => { + const unitTypeA = await unitTypeFactorySingle( + prisma, + UnitTypeEnum.oneBdrm, ); - const multiselectQuestionPreference = await createMultiselectQuestion( + const jurisdiction = await prisma.jurisdictions.create({ + data: jurisdictionFactory('applicationPostWithV2MSQ', { + featureFlags: [FeatureFlagEnum.enableV2MSQ], + }), + }); + await reservedCommunityTypeFactoryAll(jurisdiction.id, prisma); + const listing1 = await prisma.listings.create({ + data: await listingFactory(jurisdiction.id, prisma), + }); + const multiselectQuestion = await createMultiselectQuestion( jurisdiction.id, - listing1Created.id, + listing1.id, MultiselectQuestionsApplicationSectionEnum.preferences, + true, ); - const submissionDate = new Date(); const exampleAddress = addressFactory() as AddressCreate; - const dto: ApplicationCreate = { - contactPreferences: ['example contact preference'], - preferences: [ - { - multiselectQuestionId: multiselectQuestionPreference, - key: 'example key', - claimed: true, - options: [ - { - key: 'example key', - checked: true, - extraData: [ - { - type: InputType.boolean, - key: 'example key', - value: true, - }, - ], + const submissionDate = new Date(); + const applicationSelections = [ + { + multiselectQuestion: { id: multiselectQuestion.id }, + selections: [ + { + addressHolderAddress: exampleAddress, + multiselectOption: { + id: multiselectQuestion.multiselectOptions[0].id, }, - ], - }, - ], - status: ApplicationStatusEnum.submitted, - submissionType: ApplicationSubmissionTypeEnum.electronical, - applicant: { - firstName: 'applicant first name', - middleName: 'applicant middle name', - lastName: 'applicant last name', - birthMonth: '12', - birthDay: '17', - birthYear: '1993', - emailAddress: 'example@email.com', - noEmail: false, - phoneNumber: '111-111-1111', - phoneNumberType: 'Cell', - noPhone: false, - workInRegion: YesNoEnum.yes, - applicantWorkAddress: exampleAddress, - applicantAddress: exampleAddress, - }, - accessibility: { - mobility: false, - vision: false, - hearing: false, - }, - alternateContact: { - type: AlternateContactRelationship.friend, - otherType: 'example other type', - firstName: 'example first name', - lastName: 'example last name', - agency: 'example agency', - phoneNumber: '111-111-1111', - emailAddress: 'example@email.com', - address: exampleAddress, - }, - applicationsAlternateAddress: exampleAddress, - applicationsMailingAddress: exampleAddress, - listings: { - id: listing1Created.id, - }, - demographics: { - ethnicity: 'example ethnicity', - gender: 'example gender', - sexualOrientation: 'example sexual orientation', - howDidYouHear: ['example how did you hear'], - race: ['example race'], + }, + ], }, - preferredUnitTypes: [ - { - id: unitTypeA.id, - }, - ], - householdMember: [ - { - orderId: 0, - firstName: 'example first name', - middleName: 'example middle name', - lastName: 'example last name', - birthMonth: '12', - birthDay: '17', - birthYear: '1993', - sameAddress: YesNoEnum.yes, - relationship: HouseholdMemberRelationship.friend, - workInRegion: YesNoEnum.yes, - householdMemberWorkAddress: exampleAddress, - householdMemberAddress: exampleAddress, - }, - ], - appUrl: 'http://www.example.com', - additionalPhone: true, - additionalPhoneNumber: '111-111-1111', - additionalPhoneNumberType: 'example type', - householdSize: 2, - housingStatus: 'example status', - sendMailToMailingAddress: true, - householdExpectingChanges: false, - householdStudent: false, - incomeVouchers: false, - income: '36000', - incomePeriod: IncomePeriodEnum.perYear, - language: LanguagesEnum.en, - acceptedTerms: true, - submissionDate: submissionDate, - reviewStatus: ApplicationReviewStatusEnum.valid, - programs: [ - { - multiselectQuestionId: multiselectQuestionProgram, - key: 'example key', - claimed: true, - options: [ - { - key: 'example key', - checked: true, - extraData: [ - { - type: InputType.boolean, - key: 'example key', - value: true, - }, - ], - }, - ], - }, - ], - }; + ]; + + const dto: ApplicationCreate = applicationCreate( + exampleAddress, + listing1.id, + submissionDate, + unitTypeA.id, + applicationSelections, + [], + [], + ApplicationSubmissionTypeEnum.paper, + ); const res = await request(app.getHttpServer()) .post(`/applications/`) .set({ passkey: process.env.API_PASS_KEY || '' }) @@ -1437,16 +1506,20 @@ describe('Application Controller Tests', () => { data: listing1, }); - const multiselectQuestionProgram = await createMultiselectQuestion( - jurisdiction.id, - listing1Created.id, - MultiselectQuestionsApplicationSectionEnum.programs, - ); - const multiselectQuestionPreference = await createMultiselectQuestion( - jurisdiction.id, - listing1Created.id, - MultiselectQuestionsApplicationSectionEnum.preferences, - ); + const multiselectQuestionProgram = ( + await createMultiselectQuestion( + jurisdiction.id, + listing1Created.id, + MultiselectQuestionsApplicationSectionEnum.programs, + ) + ).id; + const multiselectQuestionPreference = ( + await createMultiselectQuestion( + jurisdiction.id, + listing1Created.id, + MultiselectQuestionsApplicationSectionEnum.preferences, + ) + ).id; const appA = await applicationFactory({ unitTypeId: unitTypeA.id, @@ -1615,16 +1688,20 @@ describe('Application Controller Tests', () => { data: listing1, }); - const multiselectQuestionProgram = await createMultiselectQuestion( - jurisdiction.id, - listing1Created.id, - MultiselectQuestionsApplicationSectionEnum.programs, - ); - const multiselectQuestionPreference = await createMultiselectQuestion( - jurisdiction.id, - listing1Created.id, - MultiselectQuestionsApplicationSectionEnum.preferences, - ); + const multiselectQuestionProgram = ( + await createMultiselectQuestion( + jurisdiction.id, + listing1Created.id, + MultiselectQuestionsApplicationSectionEnum.programs, + ) + ).id; + const multiselectQuestionPreference = ( + await createMultiselectQuestion( + jurisdiction.id, + listing1Created.id, + MultiselectQuestionsApplicationSectionEnum.preferences, + ) + ).id; const submissionDate = new Date(); const exampleAddress = addressFactory() as AddressCreate; @@ -1782,16 +1859,20 @@ describe('Application Controller Tests', () => { data: listing1, }); - const multiselectQuestionProgram = await createMultiselectQuestion( - jurisdiction.id, - listing1Created.id, - MultiselectQuestionsApplicationSectionEnum.programs, - ); - const multiselectQuestionPreference = await createMultiselectQuestion( - jurisdiction.id, - listing1Created.id, - MultiselectQuestionsApplicationSectionEnum.preferences, - ); + const multiselectQuestionProgram = ( + await createMultiselectQuestion( + jurisdiction.id, + listing1Created.id, + MultiselectQuestionsApplicationSectionEnum.programs, + ) + ).id; + const multiselectQuestionPreference = ( + await createMultiselectQuestion( + jurisdiction.id, + listing1Created.id, + MultiselectQuestionsApplicationSectionEnum.preferences, + ) + ).id; const submissionDate = new Date(); const exampleAddress = addressFactory() as AddressCreate; diff --git a/api/test/unit/services/application.service.spec.ts b/api/test/unit/services/application.service.spec.ts index f786e3fb28..4b32e4df65 100644 --- a/api/test/unit/services/application.service.spec.ts +++ b/api/test/unit/services/application.service.spec.ts @@ -185,52 +185,19 @@ export const mockApplicationSet = ( export const mockCreateApplicationData = ( exampleAddress: AddressCreate, submissionDate: Date, + multiselectQuestionId?: string, ): ApplicationCreate => { return { - contactPreferences: ['example contact preference'], - preferences: [ - { - key: 'example key', - claimed: true, - options: [ - { - key: 'example key', - checked: true, - extraData: [ - { - type: InputType.boolean, - key: 'example key', - value: true, - }, - ], - }, - ], - }, - ], - status: ApplicationStatusEnum.submitted, - submissionType: ApplicationSubmissionTypeEnum.electronical, - applicant: { - firstName: 'applicant first name', - middleName: 'applicant middle name', - lastName: 'applicant last name', - birthMonth: '12', - birthDay: '17', - birthYear: '1993', - emailAddress: 'example@email.com', - noEmail: false, - phoneNumber: '111-111-1111', - phoneNumberType: 'Cell', - noPhone: false, - workInRegion: YesNoEnum.yes, - applicantWorkAddress: exampleAddress, - applicantAddress: exampleAddress, - }, + acceptedTerms: true, accessibility: { mobility: false, vision: false, hearing: false, other: false, }, + additionalPhone: true, + additionalPhoneNumber: '111-111-1111', + additionalPhoneNumberType: 'example additional phone number type', alternateContact: { type: AlternateContactRelationship.other, otherType: 'example other type', @@ -241,11 +208,39 @@ export const mockCreateApplicationData = ( emailAddress: 'example@email.com', address: exampleAddress, }, + applicant: { + firstName: 'applicant first name', + middleName: 'applicant middle name', + lastName: 'applicant last name', + birthMonth: '12', + birthDay: '17', + birthYear: '1993', + emailAddress: 'example@email.com', + noEmail: false, + phoneNumber: '111-111-1111', + phoneNumberType: 'Cell', + noPhone: false, + workInRegion: YesNoEnum.yes, + applicantWorkAddress: exampleAddress, + applicantAddress: exampleAddress, + }, + applicationSelections: multiselectQuestionId + ? [ + { + multiselectQuestion: { id: multiselectQuestionId }, + selections: [ + { + addressHolderAddress: exampleAddress, + multiselectOption: { id: randomUUID() }, + }, + ], + }, + ] + : [], applicationsAlternateAddress: exampleAddress, applicationsMailingAddress: exampleAddress, - listings: { - id: randomUUID(), - }, + appUrl: 'http://www.example.com', + contactPreferences: ['example contact preference'], demographics: { ethnicity: 'example ethnicity', gender: 'example gender', @@ -253,11 +248,7 @@ export const mockCreateApplicationData = ( howDidYouHear: ['example how did you hear'], race: ['example race'], }, - preferredUnitTypes: [ - { - id: randomUUID(), - }, - ], + householdExpectingChanges: false, householdMember: [ { orderId: 0, @@ -274,22 +265,40 @@ export const mockCreateApplicationData = ( householdMemberAddress: exampleAddress, }, ], - appUrl: 'http://www.example.com', - additionalPhone: true, - additionalPhoneNumber: '111-111-1111', - additionalPhoneNumberType: 'example additional phone number type', householdSize: 2, - housingStatus: 'example housing status', - sendMailToMailingAddress: true, - householdExpectingChanges: false, householdStudent: false, + housingStatus: 'example housing status', incomeVouchers: false, income: '36000', incomePeriod: IncomePeriodEnum.perYear, language: LanguagesEnum.en, - acceptedTerms: true, - submissionDate: submissionDate, - reviewStatus: ApplicationReviewStatusEnum.valid, + listings: { + id: randomUUID(), + }, + preferences: [ + { + key: 'example key', + claimed: true, + options: [ + { + key: 'example key', + checked: true, + extraData: [ + { + type: InputType.boolean, + key: 'example key', + value: true, + }, + ], + }, + ], + }, + ], + preferredUnitTypes: [ + { + id: randomUUID(), + }, + ], programs: [ { key: 'example key', @@ -309,6 +318,11 @@ export const mockCreateApplicationData = ( ], }, ], + reviewStatus: ApplicationReviewStatusEnum.valid, + sendMailToMailingAddress: true, + status: ApplicationStatusEnum.submitted, + submissionDate: submissionDate, + submissionType: ApplicationSubmissionTypeEnum.electronical, } as ApplicationCreate; }; @@ -368,6 +382,30 @@ const detailView = { other: true, }, }, + applicationSelections: { + include: { + multiselectQuestion: true, + selections: { + include: { + addressHolderAddress: { + select: { + id: true, + placeName: true, + city: true, + county: true, + state: true, + street: true, + street2: true, + zipCode: true, + latitude: true, + longitude: true, + }, + }, + multiselectOption: true, + }, + }, + }, + }, applicationsMailingAddress: { select: { id: true, @@ -451,6 +489,7 @@ const detailView = { name: true, }, }, + listingMultiselectQuestions: true, }, }, householdMember: { @@ -563,6 +602,30 @@ const baseView = { other: true, }, }, + applicationSelections: { + include: { + multiselectQuestion: true, + selections: { + include: { + addressHolderAddress: { + select: { + id: true, + placeName: true, + city: true, + county: true, + state: true, + street: true, + street2: true, + zipCode: true, + latitude: true, + longitude: true, + }, + }, + multiselectOption: true, + }, + }, + }, + }, applicationsMailingAddress: { select: { id: true, @@ -646,6 +709,7 @@ const baseView = { name: true, }, }, + listingMultiselectQuestions: true, }, }, householdMember: { @@ -1525,6 +1589,12 @@ describe('Testing application service', () => { applicationDueDate: dayjs(new Date()).add(5, 'days').toDate(), digitalApplication: true, commonDigitalApplication: true, + jurisdictions: { + id: randomUUID(), + }, + }); + prisma.listings.update = jest.fn().mockResolvedValue({ + id: randomUUID(), }); prisma.applications.updateMany = jest.fn().mockResolvedValue({}); @@ -1554,14 +1624,12 @@ describe('Testing application service', () => { id: expect.anything(), }, include: { - jurisdictions: true, - unitGroups: true, + jurisdictions: { include: { featureFlags: true } }, listingsBuildingAddress: true, listingMultiselectQuestions: { - include: { - multiselectQuestions: true, - }, + include: { multiselectQuestions: true }, }, + unitGroups: true, }, }); @@ -1788,14 +1856,12 @@ describe('Testing application service', () => { id: expect.anything(), }, include: { - jurisdictions: true, - unitGroups: true, + jurisdictions: { include: { featureFlags: true } }, listingsBuildingAddress: true, listingMultiselectQuestions: { - include: { - multiselectQuestions: true, - }, + include: { multiselectQuestions: true }, }, + unitGroups: true, }, }); @@ -1977,157 +2043,1223 @@ describe('Testing application service', () => { expect(canOrThrowMock).not.toHaveBeenCalled(); }); - it('should error while creating an application from public site because submissions are closed', async () => { + it('should create an application from public site with MSQV2 enabled', async () => { + const multiselectQuestionId = randomUUID(); prisma.listings.findUnique = jest.fn().mockResolvedValue({ id: randomUUID(), - applicationDueDate: new Date(0), - digitalApplication: true, + applicationDueDate: dayjs(new Date()).add(5, 'days').toDate(), commonDigitalApplication: true, + digitalApplication: true, + jurisdictions: { + id: randomUUID(), + featureFlags: [{ name: FeatureFlagEnum.enableV2MSQ, active: true }], + }, + + listingMultiselectQuestions: [ + { multiselectQuestionId: multiselectQuestionId }, + ], + }); + prisma.listings.update = jest.fn().mockResolvedValue({ + id: randomUUID(), }); + prisma.applications.updateMany = jest.fn().mockResolvedValue({}); prisma.applications.create = jest.fn().mockResolvedValue({ id: randomUUID(), }); - - const exampleAddress = addressFactory() as AddressCreate; - const dto = mockCreateApplicationData(exampleAddress, new Date()); - - prisma.jurisdictions.findFirst = jest + prisma.$transaction = jest .fn() - .mockResolvedValue({ id: randomUUID() }); - - await expect( - async () => - await service.create(dto, true, { - id: 'requestingUser id', - userRoles: { isAdmin: true }, - } as unknown as User), - ).rejects.toThrowError('Listing is not open for application submission'); - - expect(prisma.listings.findUnique).toHaveBeenCalledWith({ - where: { - id: expect.anything(), - }, - include: { - jurisdictions: true, - unitGroups: true, - listingsBuildingAddress: true, - listingMultiselectQuestions: { - include: { - multiselectQuestions: true, - }, - }, - }, - }); - - expect(prisma.applications.create).not.toHaveBeenCalled(); - - expect(canOrThrowMock).not.toHaveBeenCalled(); - }); + .mockResolvedValue([ + prisma.applications.updateMany, + { id: randomUUID() }, + ]); - it('should error while creating an application from public site on a listing without common app', async () => { - prisma.listings.findUnique = jest.fn().mockResolvedValue({ + prisma.address.create = jest.fn().mockResolvedValue({ id: randomUUID(), - applicationDueDate: new Date(0), - digitalApplication: false, - commonDigitalApplication: false, }); - - prisma.applications.create = jest.fn().mockResolvedValue({ + prisma.applicationSelections.create = jest.fn().mockResolvedValue({ id: randomUUID(), }); const exampleAddress = addressFactory() as AddressCreate; - const dto = mockCreateApplicationData(exampleAddress, new Date()); + const dto = mockCreateApplicationData( + exampleAddress, + new Date(), + multiselectQuestionId, + ); prisma.jurisdictions.findFirst = jest .fn() .mockResolvedValue({ id: randomUUID() }); - await expect( - async () => - await service.create(dto, true, { - id: 'requestingUser id', - userRoles: { isAdmin: true }, - } as unknown as User), - ).rejects.toThrowError('Listing is not open for application submission'); + await service.create(dto, true, requestingUser); expect(prisma.listings.findUnique).toHaveBeenCalledWith({ where: { id: expect.anything(), }, include: { - jurisdictions: true, - unitGroups: true, + jurisdictions: { include: { featureFlags: true } }, listingsBuildingAddress: true, listingMultiselectQuestions: { - include: { - multiselectQuestions: true, - }, + include: { multiselectQuestions: true }, }, + unitGroups: true, }, }); - expect(prisma.applications.create).not.toHaveBeenCalled(); - - expect(canOrThrowMock).not.toHaveBeenCalled(); - }); - - it('should create an application from partner site', async () => { - prisma.listings.findUnique = jest.fn().mockResolvedValue({ - id: randomUUID(), - }); - prisma.applications.create = jest.fn().mockResolvedValue({ - id: randomUUID(), + expect(prisma.applications.updateMany).toHaveBeenCalledWith({ + data: { + isNewest: false, + }, + where: { + userId: requestingUser.id, + isNewest: true, + }, + }); + expect(prisma.applications.create).toHaveBeenCalledWith({ + include: { ...detailView }, + data: { + isNewest: true, + contactPreferences: ['example contact preference'], + status: ApplicationStatusEnum.submitted, + submissionType: ApplicationSubmissionTypeEnum.electronical, + appUrl: 'http://www.example.com', + additionalPhone: true, + additionalPhoneNumber: '111-111-1111', + additionalPhoneNumberType: 'example additional phone number type', + householdSize: 2, + housingStatus: 'example housing status', + sendMailToMailingAddress: true, + householdExpectingChanges: false, + householdStudent: false, + incomeVouchers: false, + income: '36000', + incomePeriod: IncomePeriodEnum.perYear, + language: LanguagesEnum.en, + acceptedTerms: true, + // Submission date is the moment it was created + submissionDate: expect.any(Date), + reviewStatus: ApplicationReviewStatusEnum.valid, + confirmationCode: expect.anything(), + applicant: { + create: { + firstName: 'applicant first name', + middleName: 'applicant middle name', + lastName: 'applicant last name', + birthMonth: 12, + birthDay: 17, + birthYear: 1993, + emailAddress: 'example@email.com', + noEmail: false, + phoneNumber: '111-111-1111', + phoneNumberType: 'Cell', + noPhone: false, + workInRegion: YesNoEnum.yes, + applicantAddress: { + create: { + ...exampleAddress, + }, + }, + applicantWorkAddress: { + create: { + ...exampleAddress, + }, + }, + }, + }, + accessibility: { + create: { + mobility: false, + vision: false, + hearing: false, + other: false, + }, + }, + alternateContact: { + create: { + type: AlternateContactRelationship.other, + otherType: 'example other type', + firstName: 'example first name', + lastName: 'example last name', + agency: 'example agency', + phoneNumber: '111-111-1111', + emailAddress: 'example@email.com', + address: { + create: { + ...exampleAddress, + }, + }, + }, + }, + applicationsAlternateAddress: { + create: { + ...exampleAddress, + }, + }, + applicationsMailingAddress: { + create: { + ...exampleAddress, + }, + }, + listings: { + connect: { + id: dto.listings.id, + }, + }, + demographics: { + create: { + ethnicity: 'example ethnicity', + gender: 'example gender', + sexualOrientation: 'example sexual orientation', + howDidYouHear: ['example how did you hear'], + race: ['example race'], + }, + }, + preferredUnitTypes: { + connect: [ + { + id: expect.anything(), + }, + ], + }, + householdMember: { + create: [ + { + orderId: 0, + firstName: 'example first name', + middleName: 'example middle name', + lastName: 'example last name', + birthMonth: 12, + birthDay: 17, + birthYear: 1993, + sameAddress: YesNoEnum.yes, + relationship: HouseholdMemberRelationship.other, + workInRegion: YesNoEnum.yes, + householdMemberAddress: { + create: { + ...exampleAddress, + }, + }, + householdMemberWorkAddress: { + create: { + ...exampleAddress, + }, + }, + }, + ], + }, + programs: [ + { + key: 'example key', + claimed: true, + options: [ + { + key: 'example key', + checked: true, + extraData: [ + { + type: InputType.boolean, + key: 'example key', + value: true, + }, + ], + }, + ], + }, + ], + preferences: [ + { + key: 'example key', + claimed: true, + options: [ + { + key: 'example key', + checked: true, + extraData: [ + { + type: InputType.boolean, + key: 'example key', + value: true, + }, + ], + }, + ], + }, + ], + userAccounts: { + connect: { + id: requestingUser.id, + }, + }, + }, + }); + + expect(prisma.applicationSelections.create).toHaveBeenCalledWith({ + data: { + applicationId: expect.anything(), + hasOptedOut: false, + multiselectQuestionId: multiselectQuestionId, + selections: { + createMany: { + data: [ + { + addressHolderAddressId: expect.anything(), + multiselectOptionId: expect.anything(), + }, + ], + }, + }, + }, + include: { + multiselectQuestion: true, + selections: { + include: { + addressHolderAddress: { + select: { + id: true, + placeName: true, + city: true, + county: true, + state: true, + street: true, + street2: true, + zipCode: true, + latitude: true, + longitude: true, + }, + }, + multiselectOption: true, + }, + }, + }, + }); + + expect(canOrThrowMock).not.toHaveBeenCalled(); + }); + + it('should error while creating an application from public site because submissions are closed', async () => { + prisma.listings.findUnique = jest.fn().mockResolvedValue({ + id: randomUUID(), + applicationDueDate: new Date(0), + digitalApplication: true, + commonDigitalApplication: true, + jurisdictions: { + id: randomUUID(), + }, + }); + + prisma.applications.create = jest.fn().mockResolvedValue({ + id: randomUUID(), + }); + + const exampleAddress = addressFactory() as AddressCreate; + const dto = mockCreateApplicationData(exampleAddress, new Date()); + + prisma.jurisdictions.findFirst = jest + .fn() + .mockResolvedValue({ id: randomUUID() }); + + await expect( + async () => + await service.create(dto, true, { + id: 'requestingUser id', + userRoles: { isAdmin: true }, + } as unknown as User), + ).rejects.toThrowError('Listing is not open for application submission'); + + expect(prisma.listings.findUnique).toHaveBeenCalledWith({ + where: { + id: expect.anything(), + }, + include: { + jurisdictions: { include: { featureFlags: true } }, + listingsBuildingAddress: true, + listingMultiselectQuestions: { + include: { multiselectQuestions: true }, + }, + unitGroups: true, + }, + }); + + expect(prisma.applications.create).not.toHaveBeenCalled(); + + expect(canOrThrowMock).not.toHaveBeenCalled(); + }); + + it('should error while creating an application from public site on a listing without common app', async () => { + prisma.listings.findUnique = jest.fn().mockResolvedValue({ + id: randomUUID(), + applicationDueDate: new Date(0), + digitalApplication: false, + commonDigitalApplication: false, + jurisdictions: { + id: randomUUID(), + }, + }); + + prisma.applications.create = jest.fn().mockResolvedValue({ + id: randomUUID(), + }); + + const exampleAddress = addressFactory() as AddressCreate; + const dto = mockCreateApplicationData(exampleAddress, new Date()); + + prisma.jurisdictions.findFirst = jest + .fn() + .mockResolvedValue({ id: randomUUID() }); + + await expect( + async () => + await service.create(dto, true, { + id: 'requestingUser id', + userRoles: { isAdmin: true }, + } as unknown as User), + ).rejects.toThrowError('Listing is not open for application submission'); + + expect(prisma.listings.findUnique).toHaveBeenCalledWith({ + where: { + id: expect.anything(), + }, + include: { + jurisdictions: { include: { featureFlags: true } }, + listingsBuildingAddress: true, + listingMultiselectQuestions: { + include: { multiselectQuestions: true }, + }, + unitGroups: true, + }, + }); + + expect(prisma.applications.create).not.toHaveBeenCalled(); + + expect(canOrThrowMock).not.toHaveBeenCalled(); + }); + + it('should create an application from partner site', async () => { + prisma.listings.findUnique = jest.fn().mockResolvedValue({ + id: randomUUID(), + jurisdictions: { + id: randomUUID(), + }, + }); + prisma.applications.create = jest.fn().mockResolvedValue({ + id: randomUUID(), + }); + prisma.listings.update = jest.fn().mockResolvedValue({ + id: randomUUID(), + }); + prisma.$transaction = jest.fn().mockResolvedValue([ + // update previous applications + jest.fn().mockResolvedValue({ + id: randomUUID(), + }), + // application create mock + jest.fn().mockResolvedValue({ + id: randomUUID(), + }), + ]); + + prisma.jurisdictions.findFirst = jest + .fn() + .mockResolvedValue({ id: randomUUID() }); + + const exampleAddress = addressFactory() as AddressCreate; + const dto = mockCreateApplicationData(exampleAddress, new Date()); + + await service.create(dto, false, { + id: 'requestingUser id', + userRoles: { isAdmin: true }, + } as unknown as User); + + expect(prisma.listings.findUnique).toHaveBeenCalledWith({ + where: { + id: expect.anything(), + }, + include: { + jurisdictions: { include: { featureFlags: true } }, + listingsBuildingAddress: true, + listingMultiselectQuestions: { + include: { multiselectQuestions: true }, + }, + unitGroups: true, + }, + }); + + expect(prisma.applications.create).toHaveBeenCalledWith({ + include: { + ...detailView, + }, + data: { + isNewest: false, + contactPreferences: ['example contact preference'], + status: ApplicationStatusEnum.submitted, + submissionType: ApplicationSubmissionTypeEnum.electronical, + appUrl: 'http://www.example.com', + additionalPhone: true, + additionalPhoneNumber: '111-111-1111', + additionalPhoneNumberType: 'example additional phone number type', + householdSize: 2, + housingStatus: 'example housing status', + sendMailToMailingAddress: true, + householdExpectingChanges: false, + householdStudent: false, + incomeVouchers: false, + income: '36000', + incomePeriod: IncomePeriodEnum.perYear, + language: LanguagesEnum.en, + acceptedTerms: true, + submissionDate: expect.anything(), + reviewStatus: ApplicationReviewStatusEnum.valid, + confirmationCode: expect.anything(), + applicant: { + create: { + firstName: 'applicant first name', + middleName: 'applicant middle name', + lastName: 'applicant last name', + birthMonth: 12, + birthDay: 17, + birthYear: 1993, + emailAddress: 'example@email.com', + noEmail: false, + phoneNumber: '111-111-1111', + phoneNumberType: 'Cell', + noPhone: false, + workInRegion: YesNoEnum.yes, + applicantAddress: { + create: { + ...exampleAddress, + }, + }, + applicantWorkAddress: { + create: { + ...exampleAddress, + }, + }, + }, + }, + accessibility: { + create: { + mobility: false, + vision: false, + hearing: false, + other: false, + }, + }, + alternateContact: { + create: { + type: AlternateContactRelationship.other, + otherType: 'example other type', + firstName: 'example first name', + lastName: 'example last name', + agency: 'example agency', + phoneNumber: '111-111-1111', + emailAddress: 'example@email.com', + address: { + create: { + ...exampleAddress, + }, + }, + }, + }, + applicationsAlternateAddress: { + create: { + ...exampleAddress, + }, + }, + applicationsMailingAddress: { + create: { + ...exampleAddress, + }, + }, + listings: { + connect: { + id: dto.listings.id, + }, + }, + demographics: { + create: { + ethnicity: 'example ethnicity', + gender: 'example gender', + sexualOrientation: 'example sexual orientation', + howDidYouHear: ['example how did you hear'], + race: ['example race'], + }, + }, + preferredUnitTypes: { + connect: [ + { + id: expect.anything(), + }, + ], + }, + householdMember: { + create: [ + { + orderId: 0, + firstName: 'example first name', + middleName: 'example middle name', + lastName: 'example last name', + birthMonth: 12, + birthDay: 17, + birthYear: 1993, + sameAddress: YesNoEnum.yes, + relationship: HouseholdMemberRelationship.other, + workInRegion: YesNoEnum.yes, + householdMemberAddress: { + create: { + ...exampleAddress, + }, + }, + householdMemberWorkAddress: { + create: { + ...exampleAddress, + }, + }, + }, + ], + }, + programs: [ + { + key: 'example key', + claimed: true, + options: [ + { + key: 'example key', + checked: true, + extraData: [ + { + type: InputType.boolean, + key: 'example key', + value: true, + }, + ], + }, + ], + }, + ], + preferences: [ + { + key: 'example key', + claimed: true, + options: [ + { + key: 'example key', + checked: true, + extraData: [ + { + type: InputType.boolean, + key: 'example key', + value: true, + }, + ], + }, + ], + }, + ], + userAccounts: { + connect: { + id: 'requestingUser id', + }, + }, + }, + }); + + expect(canOrThrowMock).toHaveBeenCalledWith( + { + id: 'requestingUser id', + userRoles: { isAdmin: true }, + } as unknown as User, + 'application', + permissionActions.create, + { + listingId: dto.listings.id, + jurisdictionId: expect.anything(), + }, + ); + }); + + it('should create an application from partner site when listing is closed', async () => { + process.env.APPLICATION_DAYS_TILL_EXPIRY = '60'; + prisma.listings.findUnique = jest.fn().mockResolvedValue({ + id: randomUUID(), + status: ListingsStatusEnum.closed, + closedAt: new Date('2024-04-28 00:00 -08:00'), + jurisdictions: { + id: randomUUID(), + }, + }); + prisma.applications.create = jest.fn().mockResolvedValue({ + id: randomUUID(), + }); + prisma.listings.update = jest.fn().mockResolvedValue({ + id: randomUUID(), + }); + prisma.$transaction = jest.fn().mockResolvedValue([ + // update previous applications + jest.fn().mockResolvedValue({ + id: randomUUID(), + }), + // application create mock + jest.fn().mockResolvedValue({ + id: randomUUID(), + }), + ]); + + prisma.jurisdictions.findFirst = jest + .fn() + .mockResolvedValue({ id: randomUUID() }); + + const exampleAddress = addressFactory() as AddressCreate; + const dto = mockCreateApplicationData(exampleAddress, new Date()); + + await service.create(dto, false, { + id: 'requestingUser id', + userRoles: { isAdmin: true }, + } as unknown as User); + + expect(prisma.applications.create).toHaveBeenCalledWith({ + include: { + ...detailView, + }, + data: { + isNewest: false, + expireAfter: new Date('2024-06-27T08:00:00.000Z'), + contactPreferences: ['example contact preference'], + status: ApplicationStatusEnum.submitted, + submissionType: ApplicationSubmissionTypeEnum.electronical, + appUrl: 'http://www.example.com', + additionalPhone: true, + additionalPhoneNumber: '111-111-1111', + additionalPhoneNumberType: 'example additional phone number type', + householdSize: 2, + housingStatus: 'example housing status', + sendMailToMailingAddress: true, + householdExpectingChanges: false, + householdStudent: false, + incomeVouchers: false, + income: '36000', + incomePeriod: IncomePeriodEnum.perYear, + language: LanguagesEnum.en, + acceptedTerms: true, + submissionDate: expect.anything(), + reviewStatus: ApplicationReviewStatusEnum.valid, + confirmationCode: expect.anything(), + applicant: { + create: { + firstName: 'applicant first name', + middleName: 'applicant middle name', + lastName: 'applicant last name', + birthMonth: 12, + birthDay: 17, + birthYear: 1993, + emailAddress: 'example@email.com', + noEmail: false, + phoneNumber: '111-111-1111', + phoneNumberType: 'Cell', + noPhone: false, + workInRegion: YesNoEnum.yes, + applicantAddress: { + create: { + ...exampleAddress, + }, + }, + applicantWorkAddress: { + create: { + ...exampleAddress, + }, + }, + }, + }, + accessibility: { + create: { + mobility: false, + vision: false, + hearing: false, + other: false, + }, + }, + alternateContact: { + create: { + type: AlternateContactRelationship.other, + otherType: 'example other type', + firstName: 'example first name', + lastName: 'example last name', + agency: 'example agency', + phoneNumber: '111-111-1111', + emailAddress: 'example@email.com', + address: { + create: { + ...exampleAddress, + }, + }, + }, + }, + applicationsAlternateAddress: { + create: { + ...exampleAddress, + }, + }, + applicationsMailingAddress: { + create: { + ...exampleAddress, + }, + }, + listings: { + connect: { + id: dto.listings.id, + }, + }, + demographics: { + create: { + ethnicity: 'example ethnicity', + gender: 'example gender', + sexualOrientation: 'example sexual orientation', + howDidYouHear: ['example how did you hear'], + race: ['example race'], + }, + }, + preferredUnitTypes: { + connect: [ + { + id: expect.anything(), + }, + ], + }, + householdMember: { + create: [ + { + orderId: 0, + firstName: 'example first name', + middleName: 'example middle name', + lastName: 'example last name', + birthMonth: 12, + birthDay: 17, + birthYear: 1993, + sameAddress: YesNoEnum.yes, + relationship: HouseholdMemberRelationship.other, + workInRegion: YesNoEnum.yes, + householdMemberAddress: { + create: { + ...exampleAddress, + }, + }, + householdMemberWorkAddress: { + create: { + ...exampleAddress, + }, + }, + }, + ], + }, + programs: [ + { + key: 'example key', + claimed: true, + options: [ + { + key: 'example key', + checked: true, + extraData: [ + { + type: InputType.boolean, + key: 'example key', + value: true, + }, + ], + }, + ], + }, + ], + preferences: [ + { + key: 'example key', + claimed: true, + options: [ + { + key: 'example key', + checked: true, + extraData: [ + { + type: InputType.boolean, + key: 'example key', + value: true, + }, + ], + }, + ], + }, + ], + userAccounts: { + connect: { + id: 'requestingUser id', + }, + }, + }, + }); + + expect(canOrThrowMock).toHaveBeenCalledWith( + { + id: 'requestingUser id', + userRoles: { isAdmin: true }, + } as unknown as User, + 'application', + permissionActions.create, + { + listingId: dto.listings.id, + jurisdictionId: expect.anything(), + }, + ); + process.env.APPLICATION_DAYS_TILL_EXPIRY = null; + }); + }); + + describe('update endpoint', () => { + it('should update an application when one exists', async () => { + prisma.applications.findUnique = jest.fn().mockResolvedValue({ + id: randomUUID(), + listingId: randomUUID(), + }); + prisma.applications.update = jest.fn().mockResolvedValue({ + id: randomUUID(), + listingId: randomUUID(), + }); + + prisma.householdMember.deleteMany = jest.fn().mockResolvedValue(null); + + prisma.listings.findUnique = jest.fn().mockResolvedValue({ + id: randomUUID(), + jurisdictions: { + id: randomUUID(), + }, + }); + prisma.listings.update = jest.fn().mockResolvedValue({ + id: randomUUID(), + }); + + prisma.$transaction = jest.fn().mockResolvedValue([ + prisma.householdMember.deleteMany, + { + id: randomUUID(), + listingId: randomUUID(), + }, + ]); + + const exampleAddress = addressFactory() as AddressCreate; + const submissionDate = new Date(); + + const dto: ApplicationUpdate = { + ...mockCreateApplicationData(exampleAddress, submissionDate), + id: randomUUID(), + applicationSelections: [], + }; + + await service.update(dto, { + id: 'requestingUser id', + userRoles: { isAdmin: true }, + } as unknown as User); + + expect(prisma.applications.findUnique).toHaveBeenCalledWith({ + include: baseView, + where: { + id: expect.anything(), + }, + }); + + expect(prisma.applications.update).toHaveBeenCalledWith({ + include: { + ...detailView, + }, + data: { + contactPreferences: ['example contact preference'], + status: ApplicationStatusEnum.submitted, + submissionType: ApplicationSubmissionTypeEnum.electronical, + appUrl: 'http://www.example.com', + additionalPhone: true, + additionalPhoneNumber: '111-111-1111', + additionalPhoneNumberType: 'example additional phone number type', + householdSize: 2, + housingStatus: 'example housing status', + sendMailToMailingAddress: true, + householdExpectingChanges: false, + householdStudent: false, + incomeVouchers: false, + income: '36000', + incomePeriod: IncomePeriodEnum.perYear, + language: LanguagesEnum.en, + acceptedTerms: true, + submissionDate: submissionDate, + reviewStatus: ApplicationReviewStatusEnum.valid, + applicant: { + create: { + firstName: 'applicant first name', + middleName: 'applicant middle name', + lastName: 'applicant last name', + birthMonth: 12, + birthDay: 17, + birthYear: 1993, + emailAddress: 'example@email.com', + noEmail: false, + phoneNumber: '111-111-1111', + phoneNumberType: 'Cell', + noPhone: false, + workInRegion: YesNoEnum.yes, + applicantAddress: { + create: { + ...exampleAddress, + }, + }, + applicantWorkAddress: { + create: { + ...exampleAddress, + }, + }, + }, + }, + accessibility: { + create: { + mobility: false, + vision: false, + hearing: false, + other: false, + }, + }, + alternateContact: { + create: { + type: AlternateContactRelationship.other, + otherType: 'example other type', + firstName: 'example first name', + lastName: 'example last name', + agency: 'example agency', + phoneNumber: '111-111-1111', + emailAddress: 'example@email.com', + address: { + create: { + ...exampleAddress, + }, + }, + }, + }, + applicationSelections: {}, + applicationsAlternateAddress: { + create: { + ...exampleAddress, + }, + }, + applicationsMailingAddress: { + create: { + ...exampleAddress, + }, + }, + listings: { + connect: { + id: dto.listings.id, + }, + }, + demographics: { + create: { + ethnicity: 'example ethnicity', + gender: 'example gender', + sexualOrientation: 'example sexual orientation', + howDidYouHear: ['example how did you hear'], + race: ['example race'], + }, + }, + preferredUnitTypes: { + set: [{ id: expect.anything() }], + }, + householdMember: { + create: [ + { + orderId: 0, + firstName: 'example first name', + middleName: 'example middle name', + lastName: 'example last name', + birthMonth: 12, + birthDay: 17, + birthYear: 1993, + sameAddress: YesNoEnum.yes, + relationship: HouseholdMemberRelationship.other, + workInRegion: YesNoEnum.yes, + householdMemberAddress: { + create: { + ...exampleAddress, + }, + }, + householdMemberWorkAddress: { + create: { + ...exampleAddress, + }, + }, + }, + ], + }, + programs: [ + { + key: 'example key', + claimed: true, + options: [ + { + key: 'example key', + checked: true, + extraData: [ + { + type: InputType.boolean, + key: 'example key', + value: true, + }, + ], + }, + ], + }, + ], + preferences: [ + { + key: 'example key', + claimed: true, + options: [ + { + key: 'example key', + checked: true, + extraData: [ + { + type: InputType.boolean, + key: 'example key', + value: true, + }, + ], + }, + ], + }, + ], + }, + where: { + id: expect.anything(), + }, + }); + + expect(prisma.listings.update).toHaveBeenCalledWith({ + where: { + id: expect.anything(), + }, + data: { + lastApplicationUpdateAt: expect.anything(), + }, + }); + + expect(canOrThrowMock).toHaveBeenCalledWith( + { + id: 'requestingUser id', + userRoles: { isAdmin: true }, + } as unknown as User, + 'application', + permissionActions.update, + expect.anything(), + ); + }); + + it.skip('should add new applicationSelection to an application with MSQV2 enabled', async () => { + const applicationId = randomUUID(); + const multiselectQuestionId = randomUUID(); + const multiselectOptionId = randomUUID(); + + prisma.address.create = jest.fn().mockResolvedValue({ + id: randomUUID(), + }); + prisma.applicationSelections.create = jest.fn().mockResolvedValue({ + id: randomUUID(), + }); + + prisma.applications.findUnique = jest.fn().mockResolvedValue({ + id: applicationId, + applicationSelections: [], + listingId: randomUUID(), + }); + prisma.applications.update = jest.fn().mockResolvedValue({ + id: applicationId, + listingId: randomUUID(), + }); + + prisma.householdMember.deleteMany = jest.fn().mockResolvedValue(null); + + prisma.listings.findUnique = jest.fn().mockResolvedValue({ + id: randomUUID(), + jurisdictions: { + id: randomUUID(), + featureFlags: [{ name: FeatureFlagEnum.enableV2MSQ, active: true }], + }, + listingMultiselectQuestions: [ + { multiselectQuestionId: multiselectQuestionId }, + ], }); prisma.listings.update = jest.fn().mockResolvedValue({ id: randomUUID(), }); + prisma.$transaction = jest.fn().mockResolvedValue([ - // update previous applications - jest.fn().mockResolvedValue({ - id: randomUUID(), - }), - // application create mock - jest.fn().mockResolvedValue({ + prisma.householdMember.deleteMany, + { id: randomUUID(), - }), + listingId: randomUUID(), + }, ]); - prisma.jurisdictions.findFirst = jest - .fn() - .mockResolvedValue({ id: randomUUID() }); - const exampleAddress = addressFactory() as AddressCreate; - const dto = mockCreateApplicationData(exampleAddress, new Date()); + const submissionDate = new Date(); - await service.create(dto, false, { + const dto: ApplicationUpdate = { + ...mockCreateApplicationData(exampleAddress, submissionDate), + id: applicationId, + applicationSelections: [ + { + application: { id: applicationId }, + hasOptedOut: false, + multiselectQuestion: { id: multiselectQuestionId }, + selections: [ + { + addressHolderAddress: exampleAddress, + multiselectOption: { id: multiselectOptionId }, + }, + ], + }, + ], + }; + + await service.update(dto, { id: 'requestingUser id', userRoles: { isAdmin: true }, } as unknown as User); - expect(prisma.listings.findUnique).toHaveBeenCalledWith({ + expect(prisma.applications.findUnique).toHaveBeenCalledWith({ + include: baseView, where: { - id: expect.anything(), + id: applicationId, }, - include: { - jurisdictions: true, - unitGroups: true, - listingsBuildingAddress: true, - listingMultiselectQuestions: { - include: { - multiselectQuestions: true, + }); + + expect(prisma.applicationSelections.create).toHaveBeenCalledWith({ + data: { + applicationId: applicationId, + hasOptedOut: false, + multiselectQuestionId: multiselectQuestionId, + selections: { + createMany: { + data: [ + { + addressHolderAddressId: expect.anything(), + multiselectOptionId: multiselectOptionId, + }, + ], }, }, }, }); - expect(prisma.applications.create).toHaveBeenCalledWith({ + expect(prisma.applications.update).toHaveBeenCalledWith({ include: { ...detailView, }, data: { - isNewest: false, contactPreferences: ['example contact preference'], status: ApplicationStatusEnum.submitted, submissionType: ApplicationSubmissionTypeEnum.electronical, @@ -2145,9 +3277,8 @@ describe('Testing application service', () => { incomePeriod: IncomePeriodEnum.perYear, language: LanguagesEnum.en, acceptedTerms: true, - submissionDate: expect.anything(), + submissionDate: submissionDate, reviewStatus: ApplicationReviewStatusEnum.valid, - confirmationCode: expect.anything(), applicant: { create: { firstName: 'applicant first name', @@ -2198,6 +3329,7 @@ describe('Testing application service', () => { }, }, }, + applicationSelections: {}, applicationsAlternateAddress: { create: { ...exampleAddress, @@ -2223,11 +3355,7 @@ describe('Testing application service', () => { }, }, preferredUnitTypes: { - connect: [ - { - id: expect.anything(), - }, - ], + set: [{ id: expect.anything() }], }, householdMember: { create: [ @@ -2293,11 +3421,18 @@ describe('Testing application service', () => { ], }, ], - userAccounts: { - connect: { - id: 'requestingUser id', - }, - }, + }, + where: { + id: applicationId, + }, + }); + + expect(prisma.listings.update).toHaveBeenCalledWith({ + where: { + id: expect.anything(), + }, + data: { + lastApplicationUpdateAt: expect.anything(), }, }); @@ -2307,57 +3442,101 @@ describe('Testing application service', () => { userRoles: { isAdmin: true }, } as unknown as User, 'application', - permissionActions.create, - { - listingId: dto.listings.id, - jurisdictionId: expect.anything(), - }, + permissionActions.update, + expect.anything(), ); }); - it('should create an application from partner site when listing is closed', async () => { - process.env.APPLICATION_DAYS_TILL_EXPIRY = '60'; - prisma.listings.findUnique = jest.fn().mockResolvedValue({ + it.skip('should remove an applicationSelection to an application with MSQV2 enabled', async () => { + const applicationId = randomUUID(); + const multiselectQuestionId = randomUUID(); + + prisma.address.create = jest.fn().mockResolvedValue({ id: randomUUID(), - status: ListingsStatusEnum.closed, - closedAt: new Date('2024-04-28 00:00 -08:00'), }); - prisma.applications.create = jest.fn().mockResolvedValue({ + + prisma.applicationSelections.deleteMany = jest + .fn() + .mockResolvedValue(null); + + prisma.applications.findUnique = jest.fn().mockResolvedValue({ + id: applicationId, + applicationSelections: [ + { + id: randomUUID(), + application: { id: applicationId }, + hasOptedOut: false, + multiselectQuestion: { id: multiselectQuestionId }, + selections: [ + { + id: randomUUID(), + multiselectOption: { id: randomUUID() }, + }, + ], + }, + ], + listingId: randomUUID(), + }); + prisma.applications.update = jest.fn().mockResolvedValue({ + id: applicationId, + listingId: randomUUID(), + }); + + prisma.householdMember.deleteMany = jest.fn().mockResolvedValue(null); + + prisma.listings.findUnique = jest.fn().mockResolvedValue({ id: randomUUID(), + jurisdictions: { + id: randomUUID(), + featureFlags: [{ name: FeatureFlagEnum.enableV2MSQ, active: true }], + }, + listingMultiselectQuestions: [ + { multiselectQuestionId: multiselectQuestionId }, + ], }); prisma.listings.update = jest.fn().mockResolvedValue({ id: randomUUID(), }); + prisma.$transaction = jest.fn().mockResolvedValue([ - // update previous applications - jest.fn().mockResolvedValue({ - id: randomUUID(), - }), - // application create mock - jest.fn().mockResolvedValue({ + prisma.applicationSelections.deleteMany, + prisma.householdMember.deleteMany, + { id: randomUUID(), - }), + listingId: randomUUID(), + }, ]); - prisma.jurisdictions.findFirst = jest - .fn() - .mockResolvedValue({ id: randomUUID() }); - const exampleAddress = addressFactory() as AddressCreate; - const dto = mockCreateApplicationData(exampleAddress, new Date()); + const submissionDate = new Date(); - await service.create(dto, false, { + const dto: ApplicationUpdate = { + ...mockCreateApplicationData(exampleAddress, submissionDate), + id: applicationId, + applicationSelections: [], + }; + + await service.update(dto, { id: 'requestingUser id', userRoles: { isAdmin: true }, } as unknown as User); - expect(prisma.applications.create).toHaveBeenCalledWith({ + expect(prisma.applications.findUnique).toHaveBeenCalledWith({ + include: baseView, + where: { + id: applicationId, + }, + }); + + expect(prisma.applicationSelections.deleteMany).toHaveBeenCalledWith({ + where: { id: { in: [expect.anything()] } }, + }); + + expect(prisma.applications.update).toHaveBeenCalledWith({ include: { ...detailView, }, data: { - isNewest: false, - expireAfter: new Date('2024-06-27T08:00:00.000Z'), contactPreferences: ['example contact preference'], status: ApplicationStatusEnum.submitted, submissionType: ApplicationSubmissionTypeEnum.electronical, @@ -2375,9 +3554,8 @@ describe('Testing application service', () => { incomePeriod: IncomePeriodEnum.perYear, language: LanguagesEnum.en, acceptedTerms: true, - submissionDate: expect.anything(), + submissionDate: submissionDate, reviewStatus: ApplicationReviewStatusEnum.valid, - confirmationCode: expect.anything(), applicant: { create: { firstName: 'applicant first name', @@ -2428,6 +3606,7 @@ describe('Testing application service', () => { }, }, }, + applicationSelections: {}, applicationsAlternateAddress: { create: { ...exampleAddress, @@ -2453,11 +3632,7 @@ describe('Testing application service', () => { }, }, preferredUnitTypes: { - connect: [ - { - id: expect.anything(), - }, - ], + set: [{ id: expect.anything() }], }, householdMember: { create: [ @@ -2523,11 +3698,18 @@ describe('Testing application service', () => { ], }, ], - userAccounts: { - connect: { - id: 'requestingUser id', - }, - }, + }, + where: { + id: applicationId, + }, + }); + + expect(prisma.listings.update).toHaveBeenCalledWith({ + where: { + id: expect.anything(), + }, + data: { + lastApplicationUpdateAt: expect.anything(), }, }); @@ -2537,58 +3719,106 @@ describe('Testing application service', () => { userRoles: { isAdmin: true }, } as unknown as User, 'application', - permissionActions.create, - { - listingId: dto.listings.id, - jurisdictionId: expect.anything(), - }, + permissionActions.update, + expect.anything(), ); - process.env.APPLICATION_DAYS_TILL_EXPIRY = null; }); - }); - describe('update endpoint', () => { - it('should update an application when one exists', async () => { - prisma.applications.findUnique = jest - .fn() - .mockResolvedValue({ id: randomUUID() }); + it.skip('should update an applicationSelection to an application with MSQV2 enabled', async () => { + const applicationId = randomUUID(); + const applicationSelectionId = randomUUID(); + const multiselectQuestionId = randomUUID(); - prisma.listings.update = jest.fn().mockResolvedValue({ + prisma.address.create = jest.fn().mockResolvedValue({ id: randomUUID(), }); + prisma.applications.findUnique = jest.fn().mockResolvedValue({ + id: applicationId, + applicationSelections: [ + { + id: applicationSelectionId, + application: { id: applicationId }, + hasOptedOut: false, + multiselectQuestion: { id: multiselectQuestionId }, + selections: [ + { + id: randomUUID(), + multiselectOption: { id: randomUUID() }, + }, + ], + }, + ], + listingId: randomUUID(), + }); prisma.applications.update = jest.fn().mockResolvedValue({ - id: randomUUID(), + id: applicationId, listingId: randomUUID(), }); + prisma.householdMember.deleteMany = jest.fn().mockResolvedValue(null); + + prisma.listings.findUnique = jest.fn().mockResolvedValue({ + id: randomUUID(), + jurisdictions: { + id: randomUUID(), + featureFlags: [{ name: FeatureFlagEnum.enableV2MSQ, active: true }], + }, + listingMultiselectQuestions: [ + { multiselectQuestionId: multiselectQuestionId }, + ], + }); + prisma.listings.update = jest.fn().mockResolvedValue({ + id: randomUUID(), + }); + + prisma.$transaction = jest.fn().mockResolvedValue([ + prisma.applicationSelections.deleteMany, + prisma.householdMember.deleteMany, + { + id: randomUUID(), + listingId: randomUUID(), + }, + ]); + const exampleAddress = addressFactory() as AddressCreate; const submissionDate = new Date(); const dto: ApplicationUpdate = { ...mockCreateApplicationData(exampleAddress, submissionDate), - id: randomUUID(), + id: applicationId, + applicationSelections: [ + { + id: applicationSelectionId, + application: { id: applicationId }, + hasOptedOut: true, + multiselectQuestion: { id: multiselectQuestionId }, + selections: [ + { + id: randomUUID(), + multiselectOption: { id: randomUUID() }, + }, + ], + }, + ], }; - prisma.jurisdictions.findFirst = jest - .fn() - .mockResolvedValue({ id: randomUUID() }); - prisma.listings.findFirst = jest - .fn() - .mockResolvedValue({ id: randomUUID() }); - prisma.householdMember.deleteMany = jest.fn().mockResolvedValue(null); - await service.update(dto, { id: 'requestingUser id', userRoles: { isAdmin: true }, } as unknown as User); expect(prisma.applications.findUnique).toHaveBeenCalledWith({ + include: baseView, where: { - id: expect.anything(), + id: applicationId, }, }); + expect(prisma.applicationSelections.deleteMany).toHaveBeenCalledWith({ + where: { id: { in: [expect.anything()] } }, + }); + expect(prisma.applications.update).toHaveBeenCalledWith({ include: { ...detailView, @@ -2663,6 +3893,7 @@ describe('Testing application service', () => { }, }, }, + applicationSelections: {}, applicationsAlternateAddress: { create: { ...exampleAddress, @@ -2756,7 +3987,7 @@ describe('Testing application service', () => { ], }, where: { - id: expect.anything(), + id: applicationId, }, }); @@ -2782,15 +4013,10 @@ describe('Testing application service', () => { it("should error trying to update an application when one doesn't exists", async () => { prisma.applications.findUnique = jest.fn().mockResolvedValue(null); + prisma.applications.update = jest.fn().mockResolvedValue(null); - prisma.listings.update = jest.fn().mockResolvedValue({ - id: randomUUID(), - }); - - prisma.applications.update = jest.fn().mockResolvedValue({ - id: randomUUID(), - listingId: randomUUID(), - }); + prisma.listings.findUnique = jest.fn().mockResolvedValue(null); + prisma.listings.update = jest.fn().mockResolvedValue(null); const exampleAddress = addressFactory() as AddressCreate; const submissionDate = new Date(); @@ -2798,12 +4024,9 @@ describe('Testing application service', () => { const dto: ApplicationUpdate = { ...mockCreateApplicationData(exampleAddress, submissionDate), id: randomUUID(), + applicationSelections: [], }; - prisma.jurisdictions.findFirst = jest - .fn() - .mockResolvedValue({ id: randomUUID() }); - await expect( async () => await service.update(dto, { @@ -2813,6 +4036,7 @@ describe('Testing application service', () => { ).rejects.toThrowError(expect.anything()); expect(prisma.applications.findUnique).toHaveBeenCalledWith({ + include: baseView, where: { id: expect.anything(), }, @@ -2852,200 +4076,7 @@ describe('Testing application service', () => { where: { id: mockedValue.id, }, - include: { - applicant: { - select: { - id: true, - firstName: true, - middleName: true, - lastName: true, - birthMonth: true, - birthDay: true, - birthYear: true, - emailAddress: true, - noEmail: true, - phoneNumber: true, - phoneNumberType: true, - noPhone: true, - workInRegion: true, - fullTimeStudent: true, - applicantAddress: { - select: { - id: true, - placeName: true, - city: true, - county: true, - state: true, - street: true, - street2: true, - zipCode: true, - latitude: true, - longitude: true, - }, - }, - applicantWorkAddress: { - select: { - id: true, - placeName: true, - city: true, - county: true, - state: true, - street: true, - street2: true, - zipCode: true, - latitude: true, - longitude: true, - }, - }, - }, - }, - accessibility: { - select: { - id: true, - mobility: true, - vision: true, - hearing: true, - other: true, - }, - }, - applicationsMailingAddress: { - select: { - id: true, - placeName: true, - city: true, - county: true, - state: true, - street: true, - street2: true, - zipCode: true, - latitude: true, - longitude: true, - }, - }, - applicationsAlternateAddress: { - select: { - id: true, - placeName: true, - city: true, - county: true, - state: true, - street: true, - street2: true, - zipCode: true, - latitude: true, - longitude: true, - }, - }, - alternateContact: { - select: { - id: true, - type: true, - otherType: true, - firstName: true, - lastName: true, - agency: true, - phoneNumber: true, - emailAddress: true, - address: { - select: { - id: true, - placeName: true, - city: true, - county: true, - state: true, - street: true, - street2: true, - zipCode: true, - latitude: true, - longitude: true, - }, - }, - }, - }, - demographics: { - select: { - id: true, - createdAt: true, - updatedAt: true, - ethnicity: true, - gender: true, - sexualOrientation: true, - howDidYouHear: true, - race: true, - }, - }, - preferredUnitTypes: { - select: { - id: true, - name: true, - numBedrooms: true, - }, - }, - listings: { - select: { - id: true, - name: true, - jurisdictions: { - select: { - id: true, - name: true, - }, - }, - }, - }, - householdMember: { - select: { - id: true, - orderId: true, - firstName: true, - middleName: true, - lastName: true, - birthMonth: true, - birthDay: true, - birthYear: true, - sameAddress: true, - relationship: true, - workInRegion: true, - fullTimeStudent: true, - householdMemberAddress: { - select: { - id: true, - placeName: true, - city: true, - county: true, - state: true, - street: true, - street2: true, - zipCode: true, - latitude: true, - longitude: true, - }, - }, - householdMemberWorkAddress: { - select: { - id: true, - placeName: true, - city: true, - county: true, - state: true, - street: true, - street2: true, - zipCode: true, - latitude: true, - longitude: true, - }, - }, - }, - }, - userAccounts: { - select: { - id: true, - firstName: true, - lastName: true, - email: true, - }, - }, - }, + include: detailView, }); }); }); diff --git a/shared-helpers/src/types/backend-swagger.ts b/shared-helpers/src/types/backend-swagger.ts index ded40ad6b2..a24c708052 100644 --- a/shared-helpers/src/types/backend-swagger.ts +++ b/shared-helpers/src/types/backend-swagger.ts @@ -5722,7 +5722,7 @@ export interface HouseholdMember { householdMemberAddress: Address } -export interface ApplicationSelectionOptions { +export interface ApplicationSelectionOption { /** */ id: string @@ -5733,7 +5733,7 @@ export interface ApplicationSelectionOptions { updatedAt: Date /** */ - addressHolderAddress: IdDTO + addressHolderAddress: Address /** */ addressHolderName?: string @@ -5751,7 +5751,7 @@ export interface ApplicationSelectionOptions { multiselectOption: IdDTO } -export interface ApplicationSelections { +export interface ApplicationSelection { /** */ id: string @@ -5771,7 +5771,7 @@ export interface ApplicationSelections { multiselectQuestion: IdDTO /** */ - selections: ApplicationSelectionOptions + selections: ApplicationSelectionOption } export interface ApplicationMultiselectQuestionOption { @@ -5929,7 +5929,7 @@ export interface Application { householdMember: HouseholdMember[] /** */ - applicationSelections?: ApplicationSelections[] + applicationSelections?: ApplicationSelection[] /** */ preferences?: ApplicationMultiselectQuestion[] @@ -6769,7 +6769,7 @@ export interface PublicAppsFiltered { householdMember: HouseholdMember[] /** */ - applicationSelections?: ApplicationSelections[] + applicationSelections?: ApplicationSelection[] /** */ preferences?: ApplicationMultiselectQuestion[] @@ -6809,91 +6809,91 @@ export interface PublicAppsViewResponse { applicationsCount: PublicAppsCount } -export interface ApplicantUpdate { +export interface AccessibilityUpdate { /** */ - firstName?: string + mobility?: boolean /** */ - middleName?: string + vision?: boolean /** */ - lastName?: string + hearing?: boolean /** */ - birthMonth?: string + other?: boolean +} +export interface AlternateContactUpdate { /** */ - birthDay?: string + type?: AlternateContactRelationship /** */ - birthYear?: string + otherType?: string /** */ - emailAddress?: string + firstName?: string /** */ - noEmail?: boolean + lastName?: string /** */ - phoneNumber?: string + agency?: string /** */ - phoneNumberType?: string + phoneNumber?: string /** */ - noPhone?: boolean + emailAddress?: string /** */ - workInRegion?: YesNoEnum + address: AddressCreate +} +export interface ApplicantUpdate { /** */ - fullTimeStudent?: YesNoEnum + firstName?: string /** */ - applicantAddress: AddressCreate + middleName?: string /** */ - applicantWorkAddress: AddressCreate -} + lastName?: string -export interface AlternateContactUpdate { /** */ - type?: AlternateContactRelationship + birthMonth?: string /** */ - otherType?: string + birthDay?: string /** */ - firstName?: string + birthYear?: string /** */ - lastName?: string + emailAddress?: string /** */ - agency?: string + noEmail?: boolean /** */ phoneNumber?: string /** */ - emailAddress?: string + phoneNumberType?: string /** */ - address: AddressCreate -} + noPhone?: boolean -export interface AccessibilityUpdate { /** */ - mobility?: boolean + workInRegion?: YesNoEnum /** */ - vision?: boolean + fullTimeStudent?: YesNoEnum /** */ - hearing?: boolean + applicantAddress: AddressCreate /** */ - other?: boolean + applicantWorkAddress: AddressCreate } export interface DemographicUpdate { @@ -6957,6 +6957,69 @@ export interface HouseholdMemberUpdate { householdMemberWorkAddress?: AddressCreate } +export interface AddressUpdate { + /** */ + placeName?: string + + /** */ + city: string + + /** */ + county?: string + + /** */ + state: string + + /** */ + street: string + + /** */ + street2?: string + + /** */ + zipCode: string + + /** */ + latitude?: number + + /** */ + longitude?: number + + /** */ + id?: string +} + +export interface ApplicationSelectionOptionCreate { + /** */ + addressHolderName?: string + + /** */ + addressHolderRelationship?: string + + /** */ + isGeocodingVerified?: boolean + + /** */ + multiselectOption: IdDTO + + /** */ + addressHolderAddress?: AddressUpdate + + /** */ + applicationSelection?: IdDTO +} + +export interface ApplicationSelectionCreate { + /** */ + hasOptedOut?: boolean + + /** */ + multiselectQuestion: IdDTO + + /** */ + selections: ApplicationSelectionOptionCreate[] +} + export interface ApplicationCreate { /** */ appUrl?: string @@ -7024,9 +7087,6 @@ export interface ApplicationCreate { /** */ reviewStatus?: ApplicationReviewStatusEnum - /** */ - applicationSelections?: ApplicationSelections[] - /** */ preferences?: ApplicationMultiselectQuestion[] @@ -7040,19 +7100,19 @@ export interface ApplicationCreate { isNewest?: boolean /** */ - applicant: ApplicantUpdate + accessibility: AccessibilityUpdate /** */ - applicationsMailingAddress: AddressCreate + alternateContact: AlternateContactUpdate /** */ - applicationsAlternateAddress: AddressCreate + applicant: ApplicantUpdate /** */ - alternateContact: AlternateContactUpdate + applicationsMailingAddress: AddressCreate /** */ - accessibility: AccessibilityUpdate + applicationsAlternateAddress: AddressCreate /** */ demographics: DemographicUpdate @@ -7062,6 +7122,49 @@ export interface ApplicationCreate { /** */ preferredUnitTypes: IdDTO[] + + /** */ + applicationSelections?: ApplicationSelectionCreate[] +} + +export interface ApplicationSelectionOptionUpdate { + /** */ + addressHolderName?: string + + /** */ + addressHolderRelationship?: string + + /** */ + isGeocodingVerified?: boolean + + /** */ + multiselectOption: IdDTO + + /** */ + id?: string + + /** */ + addressHolderAddress?: AddressUpdate + + /** */ + applicationSelection?: IdDTO +} + +export interface ApplicationSelectionUpdate { + /** */ + application: IdDTO + + /** */ + hasOptedOut?: boolean + + /** */ + multiselectQuestion: IdDTO + + /** */ + id?: string + + /** */ + selections: ApplicationSelectionOptionUpdate[] } export interface ApplicationUpdate { @@ -7134,9 +7237,6 @@ export interface ApplicationUpdate { /** */ reviewStatus?: ApplicationReviewStatusEnum - /** */ - applicationSelections?: ApplicationSelections[] - /** */ preferences?: ApplicationMultiselectQuestion[] @@ -7150,19 +7250,22 @@ export interface ApplicationUpdate { isNewest?: boolean /** */ - applicant: ApplicantUpdate + accessibility: AccessibilityUpdate /** */ - applicationsMailingAddress: AddressCreate + alternateContact: AlternateContactUpdate /** */ - applicationsAlternateAddress: AddressCreate + applicant: ApplicantUpdate /** */ - alternateContact: AlternateContactUpdate + applicationSelections?: ApplicationSelectionUpdate[] /** */ - accessibility: AccessibilityUpdate + applicationsMailingAddress: AddressCreate + + /** */ + applicationsAlternateAddress: AddressCreate /** */ demographics: DemographicUpdate diff --git a/sites/partners/src/lib/applications/formatApplicationData.ts b/sites/partners/src/lib/applications/formatApplicationData.ts index 52b9658f48..1312217d31 100644 --- a/sites/partners/src/lib/applications/formatApplicationData.ts +++ b/sites/partners/src/lib/applications/formatApplicationData.ts @@ -21,10 +21,10 @@ import { AddressCreate, IncomePeriodEnum, ApplicationStatusEnum, - ApplicationUpdate, Accessibility, Listing, MultiselectQuestionsApplicationSectionEnum, + Application, } from "@bloom-housing/shared-helpers/src/types/backend-swagger" dayjs.extend(customParseFormat) @@ -257,7 +257,7 @@ export const mapFormToApi = ({ Format data which comes from the API into correct react-hook-form format. */ -export const mapApiToForm = (applicationData: ApplicationUpdate, listing: Listing) => { +export const mapApiToForm = (applicationData: Application, listing: Listing) => { const submissionDate = applicationData.submissionDate ? dayjs(new Date(applicationData.submissionDate)) : null