Skip to content

Commit 6ec8b71

Browse files
KrissDrawingludtkemorgan
authored andcommitted
feat: add additional neighborhood amenities (bloom-housing#5563)
* feat: add additional neighborhood amenities * test: add new neighborhood amenities to listing service test * fix: switch senior center to senior centers
1 parent f5b4347 commit 6ec8b71

15 files changed

Lines changed: 369 additions & 0 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
-- AlterEnum
2+
ALTER TYPE "neighborhood_amenities_enum" ADD VALUE 'shoppingVenues';
3+
ALTER TYPE "neighborhood_amenities_enum" ADD VALUE 'hospitals';
4+
ALTER TYPE "neighborhood_amenities_enum" ADD VALUE 'seniorCenters';
5+
ALTER TYPE "neighborhood_amenities_enum" ADD VALUE 'recreationalFacilities';
6+
ALTER TYPE "neighborhood_amenities_enum" ADD VALUE 'playgrounds';
7+
ALTER TYPE "neighborhood_amenities_enum" ADD VALUE 'busStops';
8+
9+
-- AlterTable
10+
ALTER TABLE "listing_neighborhood_amenities" ADD COLUMN "bus_stops" TEXT,
11+
ADD COLUMN "hospitals" TEXT,
12+
ADD COLUMN "playgrounds" TEXT,
13+
ADD COLUMN "recreational_facilities" TEXT,
14+
ADD COLUMN "senior_centers" TEXT,
15+
ADD COLUMN "shopping_venues" TEXT;

api/prisma/schema.prisma

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,6 +1087,12 @@ model ListingNeighborhoodAmenities {
10871087
parksAndCommunityCenters String? @map("parks_and_community_centers")
10881088
schools String?
10891089
publicTransportation String? @map("public_transportation")
1090+
shoppingVenues String? @map("shopping_venues")
1091+
hospitals String?
1092+
seniorCenters String? @map("senior_centers")
1093+
recreationalFacilities String? @map("recreational_facilities")
1094+
playgrounds String?
1095+
busStops String? @map("bus_stops")
10901096
listings Listings?
10911097
10921098
@@map("listing_neighborhood_amenities")
@@ -1415,6 +1421,12 @@ enum NeighborhoodAmenitiesEnum {
14151421
parksAndCommunityCenters
14161422
pharmacies
14171423
healthCareResources
1424+
shoppingVenues
1425+
hospitals
1426+
seniorCenters
1427+
recreationalFacilities
1428+
playgrounds
1429+
busStops
14181430
14191431
@@map("neighborhood_amenities_enum")
14201432
}

api/prisma/seed-staging.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,12 @@ export const stagingSeed = async (
281281
visibleNeighborhoodAmenities: [
282282
NeighborhoodAmenitiesEnum.groceryStores,
283283
NeighborhoodAmenitiesEnum.pharmacies,
284+
NeighborhoodAmenitiesEnum.shoppingVenues,
285+
NeighborhoodAmenitiesEnum.hospitals,
286+
NeighborhoodAmenitiesEnum.seniorCenters,
287+
NeighborhoodAmenitiesEnum.recreationalFacilities,
288+
NeighborhoodAmenitiesEnum.playgrounds,
289+
NeighborhoodAmenitiesEnum.busStops,
284290
],
285291

286292
requiredListingFields: [

api/src/dtos/listings/listing-neighborhood-amenities.dto.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,40 @@ export class ListingNeighborhoodAmenities {
3939
@IsString({ groups: [ValidationsGroupsEnum.default] })
4040
@ApiPropertyOptional()
4141
healthCareResources?: string | null;
42+
43+
@Expose()
44+
@IsOptional({ groups: [ValidationsGroupsEnum.default] })
45+
@IsString({ groups: [ValidationsGroupsEnum.default] })
46+
@ApiPropertyOptional()
47+
shoppingVenues?: string | null;
48+
49+
@Expose()
50+
@IsOptional({ groups: [ValidationsGroupsEnum.default] })
51+
@IsString({ groups: [ValidationsGroupsEnum.default] })
52+
@ApiPropertyOptional()
53+
hospitals?: string | null;
54+
55+
@Expose()
56+
@IsOptional({ groups: [ValidationsGroupsEnum.default] })
57+
@IsString({ groups: [ValidationsGroupsEnum.default] })
58+
@ApiPropertyOptional()
59+
seniorCenters?: string | null;
60+
61+
@Expose()
62+
@IsOptional({ groups: [ValidationsGroupsEnum.default] })
63+
@IsString({ groups: [ValidationsGroupsEnum.default] })
64+
@ApiPropertyOptional()
65+
recreationalFacilities?: string | null;
66+
67+
@Expose()
68+
@IsOptional({ groups: [ValidationsGroupsEnum.default] })
69+
@IsString({ groups: [ValidationsGroupsEnum.default] })
70+
@ApiPropertyOptional()
71+
playgrounds?: string | null;
72+
73+
@Expose()
74+
@IsOptional({ groups: [ValidationsGroupsEnum.default] })
75+
@IsString({ groups: [ValidationsGroupsEnum.default] })
76+
@ApiPropertyOptional()
77+
busStops?: string | null;
4278
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,30 @@ export class ListingCsvExporterService implements CsvExporterServiceInterface {
866866
path: 'listingNeighborhoodAmenities.healthCareResources',
867867
label: 'Neighborhood Amenities - Health Care Resources',
868868
},
869+
[NeighborhoodAmenitiesEnum.shoppingVenues]: {
870+
path: 'listingNeighborhoodAmenities.shoppingVenues',
871+
label: 'Neighborhood Amenities - Shopping Venues',
872+
},
873+
[NeighborhoodAmenitiesEnum.hospitals]: {
874+
path: 'listingNeighborhoodAmenities.hospitals',
875+
label: 'Neighborhood Amenities - Hospitals',
876+
},
877+
[NeighborhoodAmenitiesEnum.seniorCenters]: {
878+
path: 'listingNeighborhoodAmenities.seniorCenters',
879+
label: 'Neighborhood Amenities - Senior Centers',
880+
},
881+
[NeighborhoodAmenitiesEnum.recreationalFacilities]: {
882+
path: 'listingNeighborhoodAmenities.recreationalFacilities',
883+
label: 'Neighborhood Amenities - Recreational Facilities',
884+
},
885+
[NeighborhoodAmenitiesEnum.playgrounds]: {
886+
path: 'listingNeighborhoodAmenities.playgrounds',
887+
label: 'Neighborhood Amenities - Playgrounds',
888+
},
889+
[NeighborhoodAmenitiesEnum.busStops]: {
890+
path: 'listingNeighborhoodAmenities.busStops',
891+
label: 'Neighborhood Amenities - Bus Stops',
892+
},
869893
};
870894

871895
Object.keys(amenityHeaderMap).forEach((key) => {

api/test/unit/services/listing.service.spec.ts

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,12 @@ describe('Testing listing service', () => {
573573
parksAndCommunityCenters: 'parks',
574574
schools: 'schools',
575575
publicTransportation: 'public transportation',
576+
busStops: 'bus stops',
577+
hospitals: 'hospitals',
578+
playgrounds: 'playgrounds',
579+
recreationalFacilities: 'recreational facilities',
580+
seniorCenters: 'senior centers',
581+
shoppingVenues: 'shopping venues',
576582
},
577583
marketingType: undefined,
578584
};
@@ -3510,6 +3516,12 @@ describe('Testing listing service', () => {
35103516
parksAndCommunityCenters: 'parks',
35113517
schools: 'schools',
35123518
publicTransportation: 'public transportation',
3519+
busStops: 'bus stops',
3520+
hospitals: 'hospitals',
3521+
playgrounds: 'playgrounds',
3522+
recreationalFacilities: 'recreational facilities',
3523+
seniorCenters: 'senior centers',
3524+
shoppingVenues: 'shopping venues',
35133525
},
35143526
},
35153527
jurisdictions: {
@@ -4012,6 +4024,12 @@ describe('Testing listing service', () => {
40124024
parksAndCommunityCenters: 'parks',
40134025
schools: 'schools',
40144026
publicTransportation: 'public transportation',
4027+
busStops: 'bus stops',
4028+
hospitals: 'hospitals',
4029+
playgrounds: 'playgrounds',
4030+
recreationalFacilities: 'recreational facilities',
4031+
seniorCenters: 'senior centers',
4032+
shoppingVenues: 'shopping venues',
40154033
},
40164034
},
40174035
jurisdictions: {
@@ -4411,6 +4429,168 @@ describe('Testing listing service', () => {
44114429
},
44124430
});
44134431
});
4432+
4433+
it('should do a complete listing update', async () => {
4434+
prisma.listings.findUnique = jest.fn().mockResolvedValue({
4435+
id: 'example id',
4436+
name: 'example name',
4437+
reservedCommunityTypes: {
4438+
id: randomUUID(),
4439+
},
4440+
});
4441+
prisma.listings.update = jest.fn().mockResolvedValue({
4442+
id: 'example id',
4443+
name: 'example name',
4444+
});
4445+
prisma.listingEvents.findMany = jest.fn().mockResolvedValue([]);
4446+
prisma.listingEvents.update = jest.fn().mockResolvedValue({
4447+
id: 'example id',
4448+
name: 'example name',
4449+
});
4450+
prisma.assets.delete = jest.fn().mockResolvedValue({
4451+
id: 'example id',
4452+
name: 'example name',
4453+
});
4454+
const updateMock = jest
4455+
.fn()
4456+
.mockResolvedValue({ id: 'example id', name: 'example name' });
4457+
4458+
prisma.$transaction = jest
4459+
.fn()
4460+
.mockResolvedValue([
4461+
jest.fn(),
4462+
jest.fn(),
4463+
jest.fn(),
4464+
jest.fn(),
4465+
jest.fn(),
4466+
updateMock,
4467+
]);
4468+
4469+
const val = constructFullListingData(randomUUID());
4470+
prisma.assets.create = jest.fn().mockResolvedValue({ id: randomUUID() });
4471+
prisma.address.create = jest.fn().mockResolvedValue({ id: randomUUID() });
4472+
val.reservedCommunityTypes = null;
4473+
4474+
await service.update(val as ListingUpdate, user);
4475+
4476+
const nestedUtilitiesUpdate = {
4477+
water: false,
4478+
gas: true,
4479+
trash: false,
4480+
sewer: true,
4481+
electricity: false,
4482+
cable: true,
4483+
phone: false,
4484+
internet: true,
4485+
};
4486+
const nestedFeaturesUpdate = {
4487+
elevator: true,
4488+
wheelchairRamp: false,
4489+
serviceAnimalsAllowed: true,
4490+
accessibleParking: false,
4491+
parkingOnSite: true,
4492+
inUnitWasherDryer: false,
4493+
laundryInBuilding: true,
4494+
barrierFreeEntrance: false,
4495+
rollInShower: true,
4496+
grabBars: false,
4497+
heatingInUnit: true,
4498+
acInUnit: false,
4499+
hearing: true,
4500+
visual: false,
4501+
mobility: true,
4502+
barrierFreeUnitEntrance: false,
4503+
loweredLightSwitch: true,
4504+
barrierFreeBathroom: false,
4505+
wideDoorways: true,
4506+
loweredCabinets: false,
4507+
};
4508+
const nestedNeighborhoodAmenities = {
4509+
groceryStores: 'stores',
4510+
pharmacies: 'pharmacies',
4511+
healthCareResources: 'health care',
4512+
parksAndCommunityCenters: 'parks',
4513+
schools: 'schools',
4514+
publicTransportation: 'public transportation',
4515+
busStops: 'bus stops',
4516+
hospitals: 'hospitals',
4517+
playgrounds: 'playgrounds',
4518+
recreationalFacilities: 'recreational facilities',
4519+
seniorCenters: 'senior centers',
4520+
shoppingVenues: 'shopping venues',
4521+
};
4522+
4523+
const calculatedUnitsAvailable = service.calculateUnitsAvailable(
4524+
val.reviewOrderType,
4525+
val.units,
4526+
val.unitGroups,
4527+
);
4528+
4529+
// Capture mock call arguments instead of using expect.anything()
4530+
expect(prisma.listings.update).toHaveBeenCalledTimes(1);
4531+
const updateCall = (prisma.listings.update as jest.Mock).mock.calls[0][0];
4532+
4533+
// Test key properties
4534+
expect(updateCall.data.unitsAvailable).toBe(calculatedUnitsAvailable);
4535+
expect(updateCall.data.publishedAt).toBeInstanceOf(Date);
4536+
expect(updateCall.data.contentUpdatedAt).toBeInstanceOf(Date);
4537+
expect(updateCall.data.assets).toEqual([exampleAsset]);
4538+
expect(updateCall.data.section8Acceptance).toBe(true);
4539+
expect(updateCall.data.isVerified).toBe(true);
4540+
4541+
// Test nested structures
4542+
expect(updateCall.data.applicationMethods.create).toHaveLength(1);
4543+
expect(updateCall.data.applicationMethods.create[0].type).toBe(
4544+
ApplicationMethodsTypeEnum.Internal,
4545+
);
4546+
expect(updateCall.data.listingEvents.create).toHaveLength(1);
4547+
expect(updateCall.data.listingEvents.create[0].type).toBe(
4548+
ListingEventsTypeEnum.openHouse,
4549+
);
4550+
expect(updateCall.data.units.create).toHaveLength(1);
4551+
expect(updateCall.data.units.create[0].amiPercentage).toBe('1');
4552+
expect(updateCall.data.units.create[0].bmrProgramChart).toBe(true);
4553+
expect(updateCall.data.unitGroups.create).toEqual([]);
4554+
expect(updateCall.data.listingUtilities.upsert.create).toEqual(
4555+
nestedUtilitiesUpdate,
4556+
);
4557+
expect(updateCall.data.listingUtilities.upsert.update).toEqual(
4558+
nestedUtilitiesUpdate,
4559+
);
4560+
expect(updateCall.data.listingFeatures.upsert.create).toEqual(
4561+
nestedFeaturesUpdate,
4562+
);
4563+
expect(updateCall.data.listingFeatures.upsert.update).toEqual(
4564+
nestedFeaturesUpdate,
4565+
);
4566+
expect(
4567+
updateCall.data.listingNeighborhoodAmenities.upsert.create,
4568+
).toEqual(nestedNeighborhoodAmenities);
4569+
expect(
4570+
updateCall.data.listingNeighborhoodAmenities.upsert.update,
4571+
).toEqual(nestedNeighborhoodAmenities);
4572+
expect(updateCall.data.unitsSummary.create).toHaveLength(1);
4573+
expect(updateCall.data.unitsSummary.create[0].monthlyRentMin).toBe(1);
4574+
expect(updateCall.data.unitsSummary.create[0].totalCount).toBe(13);
4575+
4576+
// Test ID types instead of exact values
4577+
expect(typeof updateCall.where.id).toBe('string');
4578+
expect(typeof updateCall.data.units.create[0].unitTypes.connect.id).toBe(
4579+
'string',
4580+
);
4581+
expect(
4582+
typeof updateCall.data.unitsSummary.create[0].unitTypes.connect.id,
4583+
).toBe('string');
4584+
4585+
expect(canOrThrowMock).toHaveBeenCalledWith(
4586+
user,
4587+
'listing',
4588+
permissionActions.update,
4589+
{
4590+
id: 'example id',
4591+
},
4592+
);
4593+
});
44144594
});
44154595

44164596
describe('Test delete endpoint', () => {
@@ -4718,6 +4898,12 @@ describe('Testing listing service', () => {
47184898
pharmacies: null,
47194899
publicTransportation: null,
47204900
schools: null,
4901+
busStops: null,
4902+
hospitals: null,
4903+
playgrounds: null,
4904+
recreationalFacilities: null,
4905+
seniorCenters: null,
4906+
shoppingVenues: null,
47214907
},
47224908
update: {
47234909
groceryStores: null,
@@ -4726,6 +4912,12 @@ describe('Testing listing service', () => {
47264912
pharmacies: null,
47274913
publicTransportation: null,
47284914
schools: null,
4915+
busStops: null,
4916+
hospitals: null,
4917+
playgrounds: null,
4918+
recreationalFacilities: null,
4919+
seniorCenters: null,
4920+
shoppingVenues: null,
47294921
},
47304922
where: {
47314923
id: undefined,
@@ -4868,6 +5060,12 @@ describe('Testing listing service', () => {
48685060
pharmacies: null,
48695061
publicTransportation: null,
48705062
schools: null,
5063+
busStops: null,
5064+
hospitals: null,
5065+
playgrounds: null,
5066+
recreationalFacilities: null,
5067+
seniorCenters: null,
5068+
shoppingVenues: null,
48715069
},
48725070
update: {
48735071
groceryStores: null,
@@ -4876,6 +5074,12 @@ describe('Testing listing service', () => {
48765074
pharmacies: null,
48775075
publicTransportation: null,
48785076
schools: null,
5077+
busStops: null,
5078+
hospitals: null,
5079+
playgrounds: null,
5080+
recreationalFacilities: null,
5081+
seniorCenters: null,
5082+
shoppingVenues: null,
48795083
},
48805084
where: {
48815085
id: undefined,

shared-helpers/src/locales/ar.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,12 @@
644644
"listings.amenities.pharmacies": "الصيدليات",
645645
"listings.amenities.publicTransportation": "المواصلات العامة",
646646
"listings.amenities.schools": "المدارس",
647+
"listings.amenities.shoppingVenues": "أماكن التسوق",
648+
"listings.amenities.hospitals": "المستشفيات",
649+
"listings.amenities.seniorCenters": "مراكز كبار السن",
650+
"listings.amenities.recreationalFacilities": "المرافق الترفيهية",
651+
"listings.amenities.playgrounds": "ملاعب",
652+
"listings.amenities.busStops": "محطات الحافلات",
647653
"listings.annualIncome": "%{income} في السنة",
648654
"listings.applicationAlreadySubmitted": "لقد تم تقديم هذا الطلب بالفعل.",
649655
"listings.applicationDeadline": "تاريخ استحقاق الطلب",

0 commit comments

Comments
 (0)