Skip to content

Commit 8c0f6e4

Browse files
fix: neighborhood amenities remove sections (bloom-housing#5536)
* feat: add visible neighborhood amenities to jurisdictions and update related components * fix: add option to seed visibleNeighborhoodAmenities * fix: show just visible neighborhood amenities in csv export * test: fix * refactor: use enum to map amenities * fix: hide neighborhood amenities section when no jurisdiction selected --------- Co-authored-by: Emily Jablonski <65367387+emilyjablonski@users.noreply.github.com>
1 parent 8dbd82b commit 8c0f6e4

14 files changed

Lines changed: 306 additions & 205 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- CreateEnum
2+
CREATE TYPE "neighborhood_amenities_enum" AS ENUM ('groceryStores', 'publicTransportation', 'schools', 'parksAndCommunityCenters', 'pharmacies', 'healthCareResources');
3+
4+
-- AlterTable
5+
ALTER TABLE "jurisdictions" ADD COLUMN "visible_neighborhood_amenities" "neighborhood_amenities_enum"[] DEFAULT ARRAY['groceryStores', 'publicTransportation', 'schools', 'parksAndCommunityCenters', 'pharmacies', 'healthCareResources']::"neighborhood_amenities_enum"[];

api/prisma/schema.prisma

Lines changed: 51 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,12 @@ model ApplicationSelectionOptions {
178178
addressHolderAddressId String? @unique() @map("address_holder_address_id") @db.Uuid
179179
addressHolderName String? @map("address_holder_name")
180180
addressHolderRelationship String? @map("address_holder_relationship")
181-
applicationSelectionId String @map("application_selection_id") @db.Uuid
182-
isGeocodingVerified Boolean? @map("is_geocoding_verified")
183-
multiselectOptionId String @map("multiselect_option_id") @db.Uuid
184-
addressHolderAddress Address? @relation("application_selection_address", fields: [addressHolderAddressId], references: [id], onDelete: NoAction, onUpdate: NoAction)
185-
applicationSelections ApplicationSelections @relation(fields: [applicationSelectionId], references: [id], onDelete: NoAction, onUpdate: NoAction)
186-
multiselectOption MultiselectOptions @relation(fields: [multiselectOptionId], references: [id], onDelete: NoAction, onUpdate: NoAction)
181+
applicationSelectionId String @map("application_selection_id") @db.Uuid
182+
isGeocodingVerified Boolean? @map("is_geocoding_verified")
183+
multiselectOptionId String @map("multiselect_option_id") @db.Uuid
184+
addressHolderAddress Address? @relation("application_selection_address", fields: [addressHolderAddressId], references: [id], onDelete: NoAction, onUpdate: NoAction)
185+
applicationSelections ApplicationSelections @relation(fields: [applicationSelectionId], references: [id], onDelete: NoAction, onUpdate: NoAction)
186+
multiselectOption MultiselectOptions @relation(fields: [multiselectOptionId], references: [id], onDelete: NoAction, onUpdate: NoAction)
187187
188188
@@map("application_selection_options")
189189
}
@@ -195,9 +195,9 @@ model ApplicationSelections {
195195
applicationId String @map("application_id") @db.Uuid
196196
hasOptedOut Boolean? @map("has_opted_out")
197197
multiselectQuestionId String @map("multiselect_question_id") @db.Uuid
198-
application Applications @relation(fields: [applicationId], references: [id], onDelete: NoAction, onUpdate: NoAction)
199-
multiselectQuestion MultiselectQuestions @relation(fields: [multiselectQuestionId], references: [id], onDelete: NoAction, onUpdate: NoAction)
200-
selections ApplicationSelectionOptions[]
198+
application Applications @relation(fields: [applicationId], references: [id], onDelete: NoAction, onUpdate: NoAction)
199+
multiselectQuestion MultiselectQuestions @relation(fields: [multiselectQuestionId], references: [id], onDelete: NoAction, onUpdate: NoAction)
200+
selections ApplicationSelectionOptions[]
201201
202202
@@map("application_selections")
203203
}
@@ -371,35 +371,36 @@ model HouseholdMember {
371371

372372
// Note: [name] formerly max length 256
373373
model Jurisdictions {
374-
id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
375-
createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6)
376-
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6)
377-
name String @unique()
378-
notificationsSignUpUrl String? @map("notifications_sign_up_url")
379-
languages LanguagesEnum[] @default([en])
380-
partnerTerms String? @map("partner_terms")
381-
publicUrl String @default("") @map("public_url")
382-
emailFromAddress String? @map("email_from_address")
383-
rentalAssistanceDefault String @map("rental_assistance_default")
384-
whatToExpect String @default("Applicants will be contacted by the property agent in rank order until vacancies are filled. All of the information that you have provided will be verified and your eligibility confirmed. Your application will be removed from the waitlist if you have made any fraudulent statements. If we cannot verify a housing preference that you have claimed, you will not receive the preference but will not be otherwise penalized. Should your application be chosen, be prepared to fill out a more detailed application and provide required supporting documents.") @map("what_to_expect")
385-
whatToExpectAdditionalText String @default("") @map("what_to_expect_additional_text")
386-
whatToExpectUnderConstruction String @default("") @map("what_to_expect_under_construction")
387-
enablePartnerSettings Boolean @default(false) @map("enable_partner_settings")
388-
enablePartnerDemographics Boolean @default(false) @map("enable_partner_demographics")
389-
enableGeocodingPreferences Boolean @default(false) @map("enable_geocoding_preferences")
390-
enableGeocodingRadiusMethod Boolean @default(false) @map("enable_geocoding_radius_method")
391-
enableListingOpportunity Boolean @default(false) @map("enable_listing_opportunity")
392-
allowSingleUseCodeLogin Boolean @default(false) @map("allow_single_use_code_login")
374+
id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
375+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6)
376+
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6)
377+
name String @unique()
378+
notificationsSignUpUrl String? @map("notifications_sign_up_url")
379+
languages LanguagesEnum[] @default([en])
380+
partnerTerms String? @map("partner_terms")
381+
publicUrl String @default("") @map("public_url")
382+
emailFromAddress String? @map("email_from_address")
383+
rentalAssistanceDefault String @map("rental_assistance_default")
384+
whatToExpect String @default("Applicants will be contacted by the property agent in rank order until vacancies are filled. All of the information that you have provided will be verified and your eligibility confirmed. Your application will be removed from the waitlist if you have made any fraudulent statements. If we cannot verify a housing preference that you have claimed, you will not receive the preference but will not be otherwise penalized. Should your application be chosen, be prepared to fill out a more detailed application and provide required supporting documents.") @map("what_to_expect")
385+
whatToExpectAdditionalText String @default("") @map("what_to_expect_additional_text")
386+
whatToExpectUnderConstruction String @default("") @map("what_to_expect_under_construction")
387+
enablePartnerSettings Boolean @default(false) @map("enable_partner_settings")
388+
enablePartnerDemographics Boolean @default(false) @map("enable_partner_demographics")
389+
enableGeocodingPreferences Boolean @default(false) @map("enable_geocoding_preferences")
390+
enableGeocodingRadiusMethod Boolean @default(false) @map("enable_geocoding_radius_method")
391+
enableListingOpportunity Boolean @default(false) @map("enable_listing_opportunity")
392+
allowSingleUseCodeLogin Boolean @default(false) @map("allow_single_use_code_login")
393393
amiChart AmiChart[]
394394
featureFlags FeatureFlags[]
395395
multiselectQuestions MultiselectQuestions[]
396396
listings Listings[]
397397
reservedCommunityTypes ReservedCommunityTypes[]
398398
translations Translations[]
399399
user_accounts UserAccounts[]
400-
listingApprovalPermissions UserRoleEnum[] @map("listing_approval_permission")
401-
duplicateListingPermissions UserRoleEnum[] @map("duplicate_listing_permissions")
402-
requiredListingFields String[] @default([]) @map("required_listing_fields")
400+
listingApprovalPermissions UserRoleEnum[] @map("listing_approval_permission")
401+
duplicateListingPermissions UserRoleEnum[] @map("duplicate_listing_permissions")
402+
requiredListingFields String[] @default([]) @map("required_listing_fields")
403+
visibleNeighborhoodAmenities NeighborhoodAmenitiesEnum[] @default([groceryStores, publicTransportation, schools, parksAndCommunityCenters, pharmacies, healthCareResources]) @map("visible_neighborhood_amenities")
403404
404405
@@map("jurisdictions")
405406
}
@@ -977,15 +978,15 @@ model UserAccounts {
977978
}
978979

979980
model UserRoles {
980-
isAdmin Boolean @default(false) @map("is_admin")
981-
isJurisdictionalAdmin Boolean @default(false) @map("is_jurisdictional_admin")
982-
isLimitedJurisdictionalAdmin Boolean @default(false) @map("is_limited_jurisdictional_admin")
983-
isPartner Boolean @default(false) @map("is_partner")
984-
isSupportAdmin Boolean @default(false) @map("is_support_admin")
981+
isAdmin Boolean @default(false) @map("is_admin")
982+
isJurisdictionalAdmin Boolean @default(false) @map("is_jurisdictional_admin")
983+
isLimitedJurisdictionalAdmin Boolean @default(false) @map("is_limited_jurisdictional_admin")
984+
isPartner Boolean @default(false) @map("is_partner")
985+
isSupportAdmin Boolean @default(false) @map("is_support_admin")
985986
// Role for maintainers of the code base. Should have access to everything as well as developer specific pages
986-
isSuperAdmin Boolean @default(false) @map("is_super_admin")
987-
userId String @id() @map("user_id") @db.Uuid
988-
userAccounts UserAccounts @relation(fields: [userId], references: [id], onDelete: Cascade)
987+
isSuperAdmin Boolean @default(false) @map("is_super_admin")
988+
userId String @id() @map("user_id") @db.Uuid
989+
userAccounts UserAccounts @relation(fields: [userId], references: [id], onDelete: Cascade)
989990
990991
@@map("user_roles")
991992
}
@@ -1387,3 +1388,14 @@ enum ValidationMethodEnum {
13871388
13881389
@@map("validation_method_enum")
13891390
}
1391+
1392+
enum NeighborhoodAmenitiesEnum {
1393+
groceryStores
1394+
publicTransportation
1395+
schools
1396+
parksAndCommunityCenters
1397+
pharmacies
1398+
healthCareResources
1399+
1400+
@@map("neighborhood_amenities_enum")
1401+
}

api/prisma/seed-helpers/jurisdiction-factory.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { LanguagesEnum, Prisma, UserRoleEnum } from '@prisma/client';
1+
import {
2+
LanguagesEnum,
3+
NeighborhoodAmenitiesEnum,
4+
Prisma,
5+
UserRoleEnum,
6+
} from '@prisma/client';
27
import { randomName } from './word-generator';
38

49
export const jurisdictionFactory = (
@@ -9,6 +14,7 @@ export const jurisdictionFactory = (
914
featureFlags?: string[];
1015
requiredListingFields?: string[];
1116
languages?: LanguagesEnum[];
17+
visibleNeighborhoodAmenities?: NeighborhoodAmenitiesEnum[];
1218
},
1319
): Prisma.JurisdictionsCreateInput => ({
1420
name: jurisdictionName,
@@ -45,4 +51,5 @@ export const jurisdictionFactory = (
4551
}
4652
: undefined,
4753
requiredListingFields: optionalFields?.requiredListingFields || [],
54+
visibleNeighborhoodAmenities: optionalFields?.visibleNeighborhoodAmenities,
4855
});

api/prisma/seed-staging.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
ApplicationSubmissionTypeEnum,
3+
NeighborhoodAmenitiesEnum,
34
LanguagesEnum,
45
ListingsStatusEnum,
56
MultiselectQuestions,
@@ -266,7 +267,14 @@ export const stagingSeed = async (
266267
});
267268
const angelopolisJurisdiction = await prismaClient.jurisdictions.create({
268269
data: jurisdictionFactory('Angelopolis', {
269-
featureFlags: [FeatureFlagEnum.enableHousingDeveloperOwner],
270+
featureFlags: [
271+
FeatureFlagEnum.enableNeighborhoodAmenities,
272+
FeatureFlagEnum.enableHousingDeveloperOwner,
273+
],
274+
visibleNeighborhoodAmenities: [
275+
NeighborhoodAmenitiesEnum.groceryStores,
276+
NeighborhoodAmenitiesEnum.pharmacies,
277+
],
270278
requiredListingFields: [
271279
'listingsBuildingAddress',
272280
'name',

api/src/dtos/jurisdictions/jurisdiction.dto.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import {
1010
IsBoolean,
1111
} from 'class-validator';
1212
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
13-
import { LanguagesEnum, UserRoleEnum } from '@prisma/client';
13+
import {
14+
LanguagesEnum,
15+
UserRoleEnum,
16+
NeighborhoodAmenitiesEnum,
17+
} from '@prisma/client';
1418
import { FeatureFlag } from '../feature-flags/feature-flag.dto';
1519
import { AbstractDTO } from '../shared/abstract.dto';
1620
import { IdDTO } from '../shared/id.dto';
@@ -165,4 +169,18 @@ export class Jurisdiction extends AbstractDTO {
165169
@IsDefined({ groups: [ValidationsGroupsEnum.default] })
166170
@ApiProperty({ isArray: true })
167171
requiredListingFields: string[];
172+
173+
@Expose()
174+
@IsArray({ groups: [ValidationsGroupsEnum.default] })
175+
@IsEnum(NeighborhoodAmenitiesEnum, {
176+
groups: [ValidationsGroupsEnum.default],
177+
each: true,
178+
})
179+
@IsDefined({ groups: [ValidationsGroupsEnum.default] })
180+
@ApiProperty({
181+
enum: NeighborhoodAmenitiesEnum,
182+
enumName: 'NeighborhoodAmenitiesEnum',
183+
isArray: true,
184+
})
185+
visibleNeighborhoodAmenities: NeighborhoodAmenitiesEnum[];
168186
}

api/src/services/listing-csv-export.service.ts

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
ApplicationMethodsTypeEnum,
1717
ListingEventsTypeEnum,
1818
MarketingTypeEnum,
19+
NeighborhoodAmenitiesEnum,
1920
} from '@prisma/client';
2021
import { includeViews } from './listing.service';
2122
import { PrismaService } from './prisma.service';
@@ -823,34 +824,44 @@ export class ListingCsvExporterService implements CsvExporterServiceInterface {
823824
FeatureFlagEnum.enableNeighborhoodAmenities,
824825
)
825826
) {
826-
headers.push(
827-
...[
828-
{
829-
path: 'listingNeighborhoodAmenities.groceryStores',
830-
label: 'Neighborhood Amenities - Grocery Stores',
831-
},
832-
{
833-
path: 'listingNeighborhoodAmenities.publicTransportation',
834-
label: 'Neighborhood Amenities - Public Transportation',
835-
},
836-
{
837-
path: 'listingNeighborhoodAmenities.schools',
838-
label: 'Neighborhood Amenities - Schools',
839-
},
840-
{
841-
path: 'listingNeighborhoodAmenities.parksAndCommunityCenters',
842-
label: 'Neighborhood Amenities - Parks and Community Centers',
843-
},
844-
{
845-
path: 'listingNeighborhoodAmenities.pharmacies',
846-
label: 'Neighborhood Amenities - Pharmacies',
847-
},
848-
{
849-
path: 'listingNeighborhoodAmenities.healthCareResources',
850-
label: 'Neighborhood Amenities - Health Care Resources',
851-
},
852-
],
827+
const visibleAmenities = new Set<string>(
828+
(user.jurisdictions || [])
829+
.flatMap((j) => j.visibleNeighborhoodAmenities || [])
830+
.filter(Boolean),
853831
);
832+
833+
const amenityHeaderMap: Record<string, CsvHeader> = {
834+
[NeighborhoodAmenitiesEnum.groceryStores]: {
835+
path: 'listingNeighborhoodAmenities.groceryStores',
836+
label: 'Neighborhood Amenities - Grocery Stores',
837+
},
838+
[NeighborhoodAmenitiesEnum.publicTransportation]: {
839+
path: 'listingNeighborhoodAmenities.publicTransportation',
840+
label: 'Neighborhood Amenities - Public Transportation',
841+
},
842+
[NeighborhoodAmenitiesEnum.schools]: {
843+
path: 'listingNeighborhoodAmenities.schools',
844+
label: 'Neighborhood Amenities - Schools',
845+
},
846+
[NeighborhoodAmenitiesEnum.parksAndCommunityCenters]: {
847+
path: 'listingNeighborhoodAmenities.parksAndCommunityCenters',
848+
label: 'Neighborhood Amenities - Parks and Community Centers',
849+
},
850+
[NeighborhoodAmenitiesEnum.pharmacies]: {
851+
path: 'listingNeighborhoodAmenities.pharmacies',
852+
label: 'Neighborhood Amenities - Pharmacies',
853+
},
854+
[NeighborhoodAmenitiesEnum.healthCareResources]: {
855+
path: 'listingNeighborhoodAmenities.healthCareResources',
856+
label: 'Neighborhood Amenities - Health Care Resources',
857+
},
858+
};
859+
860+
Object.keys(amenityHeaderMap).forEach((key) => {
861+
if (visibleAmenities.has(key)) {
862+
headers.push(amenityHeaderMap[key]);
863+
}
864+
});
854865
}
855866

856867
headers.push(

api/test/integration/jurisdiction.e2e-spec.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Test, TestingModule } from '@nestjs/testing';
22
import { INestApplication } from '@nestjs/common';
3-
import { LanguagesEnum } from '@prisma/client';
3+
import { LanguagesEnum, NeighborhoodAmenitiesEnum } from '@prisma/client';
44
import { randomUUID } from 'crypto';
55
import request from 'supertest';
66
import cookieParser from 'cookie-parser';
@@ -135,6 +135,10 @@ describe('Jurisdiction Controller Tests', () => {
135135
listingApprovalPermissions: [],
136136
duplicateListingPermissions: [],
137137
requiredListingFields: [],
138+
visibleNeighborhoodAmenities: [
139+
NeighborhoodAmenitiesEnum.groceryStores,
140+
NeighborhoodAmenitiesEnum.pharmacies,
141+
],
138142
};
139143
const res = await request(app.getHttpServer())
140144
.post('/jurisdictions')
@@ -165,6 +169,10 @@ describe('Jurisdiction Controller Tests', () => {
165169
listingApprovalPermissions: [],
166170
duplicateListingPermissions: [],
167171
requiredListingFields: [],
172+
visibleNeighborhoodAmenities: [
173+
NeighborhoodAmenitiesEnum.groceryStores,
174+
NeighborhoodAmenitiesEnum.pharmacies,
175+
],
168176
};
169177
const res = await request(app.getHttpServer())
170178
.put(`/jurisdictions/${id}`)
@@ -199,6 +207,10 @@ describe('Jurisdiction Controller Tests', () => {
199207
listingApprovalPermissions: [],
200208
duplicateListingPermissions: [],
201209
requiredListingFields: [],
210+
visibleNeighborhoodAmenities: [
211+
NeighborhoodAmenitiesEnum.groceryStores,
212+
NeighborhoodAmenitiesEnum.pharmacies,
213+
],
202214
};
203215

204216
const res = await request(app.getHttpServer())

api/test/integration/permission-tests/helpers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export const buildJurisdictionCreateMock = (
114114
listingApprovalPermissions: [],
115115
duplicateListingPermissions: [],
116116
requiredListingFields: [],
117+
visibleNeighborhoodAmenities: [],
117118
};
118119
};
119120

@@ -138,6 +139,7 @@ export const buildJurisdictionUpdateMock = (
138139
listingApprovalPermissions: [],
139140
duplicateListingPermissions: [],
140141
requiredListingFields: [],
142+
visibleNeighborhoodAmenities: [],
141143
};
142144
};
143145

0 commit comments

Comments
 (0)