diff --git a/api/package.json b/api/package.json index 9fdf48884ec..7e09aa0ac80 100644 --- a/api/package.json +++ b/api/package.json @@ -65,6 +65,7 @@ "dayjs": "~1.11.9", "handlebars": "~4.7.8", "jsonwebtoken": "~9.0.1", + "juice": "^9.1.0", "lodash": "~4.17.21", "node-polyglot": "~2.5.0", "passport": "~0.6.0", diff --git a/api/prisma/migrations/03_0_external_listing/migration.sql b/api/prisma/migrations/03_0_external_listing/migration.sql new file mode 100644 index 00000000000..4c3f2609c93 --- /dev/null +++ b/api/prisma/migrations/03_0_external_listing/migration.sql @@ -0,0 +1,48 @@ +ALTER TABLE + "applications" DROP COLUMN "income_vouchers", +ADD + COLUMN "income_vouchers" TEXT []; + +ALTER TABLE + "demographics" +ADD + COLUMN "spoken_language" TEXT; + +ALTER TABLE + "jurisdictions" +ADD + COLUMN "enable_listing_opportunity" BOOLEAN NOT NULL DEFAULT false; + +-- CreateTable +CREATE TABLE "external_listings" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "assets" JSONB NOT NULL, + "household_size_min" INTEGER, + "household_size_max" INTEGER, + "units_available" INTEGER, + "application_due_date" TIMESTAMP(6), + "application_open_date" TIMESTAMP(6), + "name" TEXT NOT NULL, + "waitlist_current_size" INTEGER, + "waitlist_max_size" INTEGER, + "is_waitlist_open" BOOLEAN, + "status" TEXT NOT NULL, + "review_order_type" TEXT NOT NULL, + "closed_at" TIMESTAMP(6), + "updated_at" TIMESTAMP(6), + "published_at" TIMESTAMP(6), + "last_application_update_at" TIMESTAMP(6) DEFAULT '1970-01-01 00:00:00-07' :: timestamp with time zone, + "neighborhood" TEXT, + "reserved_community_type_name" TEXT, + "url_slug" TEXT NOT NULL, + "units_summarized" JSONB, + "images" JSONB, + "multiselect_questions" JSONB, + "jurisdiction" JSONB, + "reserved_community_type" JSONB, + "units" JSONB, + "building_address" JSONB, + "features" JSONB, + "utilities" JSONB, + CONSTRAINT "ExternalListing_pkey" PRIMARY KEY ("id") +); \ No newline at end of file diff --git a/api/prisma/migrations/03_external_listing/migration.sql b/api/prisma/migrations/03_1_combined_listing/migration.sql similarity index 82% rename from api/prisma/migrations/03_external_listing/migration.sql rename to api/prisma/migrations/03_1_combined_listing/migration.sql index 56bc245d23e..10745414173 100644 --- a/api/prisma/migrations/03_external_listing/migration.sql +++ b/api/prisma/migrations/03_1_combined_listing/migration.sql @@ -1,51 +1,4 @@ -ALTER TABLE - "applications" DROP COLUMN "income_vouchers", -ADD - COLUMN "income_vouchers" TEXT []; - -ALTER TABLE - "demographics" -ADD - COLUMN "spoken_language" TEXT; - -ALTER TABLE - "jurisdictions" -ADD - COLUMN "enable_listing_preferences" BOOLEAN NOT NULL DEFAULT false; - --- CreateTable -CREATE TABLE "external_listings" ( - "id" UUID NOT NULL DEFAULT uuid_generate_v4(), - "assets" JSONB NOT NULL, - "household_size_min" INTEGER, - "household_size_max" INTEGER, - "units_available" INTEGER, - "application_due_date" TIMESTAMP(6), - "application_open_date" TIMESTAMP(6), - "name" TEXT NOT NULL, - "waitlist_current_size" INTEGER, - "waitlist_max_size" INTEGER, - "is_waitlist_open" BOOLEAN, - "status" TEXT NOT NULL, - "review_order_type" TEXT NOT NULL, - "closed_at" TIMESTAMP(6), - "updated_at" TIMESTAMP(6), - "published_at" TIMESTAMP(6), - "last_application_update_at" TIMESTAMP(6) DEFAULT '1970-01-01 00:00:00-07' :: timestamp with time zone, - "neighborhood" TEXT, - "reserved_community_type_name" TEXT, - "url_slug" TEXT NOT NULL, - "units_summarized" JSONB, - "images" JSONB, - "multiselect_questions" JSONB, - "jurisdiction" JSONB, - "reserved_community_type" JSONB, - "units" JSONB, - "building_address" JSONB, - "features" JSONB, - "utilities" JSONB, - CONSTRAINT "ExternalListing_pkey" PRIMARY KEY ("id") -); +DROP TABLE IF EXISTS combined_listings; CREATE VIEW "combined_listings" AS ( SELECT @@ -67,7 +20,14 @@ CREATE VIEW "combined_listings" AS ( l.updated_at, l.last_application_update_at, l.neighborhood, + rct.name as "reserved_community_type_name", + -- TODO add reserved community type name jsonb_build_object('id', j.id, 'name', j.name) AS "jurisdiction", + CASE + WHEN rct.id IS NOT NULL THEN jsonb_build_object('name', rct.name, 'id', rct.id) + ELSE NULL + END AS "reserved_community_types", + -- TODO add reserved community types units.json AS "units", jsonb_build_object( 'city', @@ -207,6 +167,7 @@ CREATE VIEW "combined_listings" AS ( GROUP BY listing_id ) as imgs ON l.id = imgs.listing_id + LEFT JOIN reserved_community_types rct ON l.reserved_community_type_id = rct.id ) UNION ( @@ -229,16 +190,16 @@ UNION "updated_at", "last_application_update_at", "neighborhood", - -- "reserved_community_type_name", + "reserved_community_type_name", -- "url_slug", - -- "multiselect_questions", + -- "multiselect_questions", "jurisdiction", - -- "reserved_community_type", + "reserved_community_type", "units", "building_address", - -- "features", + -- "features", commenting out for now as not being used "images", - -- "utilities", + -- "utilities", commenting out for now as not being used "units_summarized" -- null -- leasing_agents; not available in base view and probably not useful anyway -- true FROM diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index e7ed8c7683e..30a79e1867f 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -866,15 +866,14 @@ view CombinedListings { lastApplicationUpdateAt DateTime? @map("last_application_update_at") @db.Timestamp(6) @default(dbgenerated("'1970-01-01 00:00:00-07'::timestamp with time zone")) neighborhood String? listingsBuildingAddress Json? @map("listings_building_address") - // reservedCommunityTypeName String? @map("reserved_community_type_name") + reservedCommunityTypeName String? @map("reserved_community_type_name") // urlSlug String @map("url_slug") unitsSummarized Json? @map("units_summarized") listingImages Json? @map("listing_images") // multiselectQuestions Json? @map("multiselect_questions") // jurisdiction Json? - // reservedCommunityType Json? @map("reserved_community_type") + reservedCommunityTypes Json? @map("reserved_community_types") units Json? - // buildingAddress Json? @map("building_address") // features Json? // utilities Json? diff --git a/api/prisma/seed-helpers/jurisdiction-factory.ts b/api/prisma/seed-helpers/jurisdiction-factory.ts index a7e70567ca5..04647d20bc5 100644 --- a/api/prisma/seed-helpers/jurisdiction-factory.ts +++ b/api/prisma/seed-helpers/jurisdiction-factory.ts @@ -17,5 +17,6 @@ export const jurisdictionFactory = ( enableAccessibilityFeatures: true, enableUtilitiesIncluded: true, enableGeocodingPreferences: true, + enableListingOpportunity: false, listingApprovalPermissions: listingApprovalPermissions || [], }); diff --git a/api/prisma/seed-helpers/translation-factory.ts b/api/prisma/seed-helpers/translation-factory.ts index 53b05df2391..608a2b47524 100644 --- a/api/prisma/seed-helpers/translation-factory.ts +++ b/api/prisma/seed-helpers/translation-factory.ts @@ -151,6 +151,31 @@ const translations = (jurisdictionName?: string) => ({ 'Use the following code to sign in to your %{jurisdictionName} account. This code will be valid for 5 minutes. Never share this code.', singleUseCode: '%{singleUseCode}', }, + rentalOpportunity: { + subject: 'New rental opportunity', + intro: 'Rental opportunity at', + applicationsDue: 'Applications Due', + community: 'Community', + address: 'Address', + rent: 'Rent', + minIncome: 'Minimum Income', + maxIncome: 'Maximum Income', + lottery: 'Lottery Date', + studio: 'Studios', + oneBdrm: '1 Bedrooms', + twoBdrm: '2 Bedrooms', + threeBdrm: '3 Bedrooms', + fourBdrm: '4 Bedrooms', + fiveBdrm: '5 Bedrooms', + SRO: 'SROs', + viewButton: { + en: 'View listing & apply', + es: 'Ver listado y aplicar', + zh: '查看列表并申请', + vi: 'Xem danh sách và áp dụng', + tl: 'Tingnan ang listahan at mag-apply', + }, + }, }); export const translationFactory = ( diff --git a/api/prisma/seed-staging.ts b/api/prisma/seed-staging.ts index fec3ac1b849..fa954ec81a4 100644 --- a/api/prisma/seed-staging.ts +++ b/api/prisma/seed-staging.ts @@ -35,7 +35,6 @@ import { simplifiedDCMap, } from './seed-helpers/map-layer-factory'; import { ValidationMethod } from '../src/enums/multiselect-questions/validation-method-enum'; -import { randomNoun } from './seed-helpers/word-generator'; export const stagingSeed = async ( prismaClient: PrismaClient, @@ -49,28 +48,28 @@ export const stagingSeed = async ( }); // add another jurisdiction const additionalJurisdiction = await prismaClient.jurisdictions.create({ - data: jurisdictionFactory('Contra Costa'), + data: jurisdictionFactory('Contra Costa', [UserRoleEnum.admin]), }); await prismaClient.jurisdictions.create({ - data: jurisdictionFactory('Marin'), + data: jurisdictionFactory('Marin', [UserRoleEnum.admin]), }); await prismaClient.jurisdictions.create({ - data: jurisdictionFactory('Napa'), + data: jurisdictionFactory('Napa', [UserRoleEnum.admin]), }); await prismaClient.jurisdictions.create({ - data: jurisdictionFactory('San Mateo'), + data: jurisdictionFactory('San Mateo', [UserRoleEnum.admin]), }); await prismaClient.jurisdictions.create({ - data: jurisdictionFactory('Santa Clara'), + data: jurisdictionFactory('Santa Clara', [UserRoleEnum.admin]), }); await prismaClient.jurisdictions.create({ - data: jurisdictionFactory('Solano'), + data: jurisdictionFactory('Solano', [UserRoleEnum.admin]), }); await prismaClient.jurisdictions.create({ - data: jurisdictionFactory('Sonoma'), + data: jurisdictionFactory('Sonoma', [UserRoleEnum.admin]), }); await prismaClient.jurisdictions.create({ - data: jurisdictionFactory('San Francisco'), + data: jurisdictionFactory('San Francisco', [UserRoleEnum.admin]), }); // create admin user await prismaClient.userAccounts.create({ diff --git a/api/src/modules/email.module.ts b/api/src/modules/email.module.ts index d1af8100d54..16052e2fbe1 100644 --- a/api/src/modules/email.module.ts +++ b/api/src/modules/email.module.ts @@ -1,4 +1,5 @@ import { Module } from '@nestjs/common'; +import { HttpModule } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; import { MailService } from '@sendgrid/mail'; import { EmailService } from '../services/email.service'; @@ -8,7 +9,7 @@ import { GoogleTranslateService } from '../services/google-translate.service'; import { SendGridService } from '../services/sendgrid.service'; @Module({ - imports: [], + imports: [HttpModule], controllers: [], providers: [ EmailService, diff --git a/api/src/services/email.service.ts b/api/src/services/email.service.ts index 86e62194cb0..34a3d601555 100644 --- a/api/src/services/email.service.ts +++ b/api/src/services/email.service.ts @@ -1,9 +1,13 @@ -import { HttpException, Injectable } from '@nestjs/common'; +import { HttpException, Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { HttpService } from '@nestjs/axios'; import { ResponseError } from '@sendgrid/helpers/classes'; import { MailDataRequired } from '@sendgrid/helpers/classes/mail'; import fs from 'fs'; import Handlebars from 'handlebars'; +import juice from 'juice'; import Polyglot from 'node-polyglot'; +import { firstValueFrom } from 'rxjs'; import path from 'path'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; @@ -12,12 +16,17 @@ import advanced from 'dayjs/plugin/advancedFormat'; import { TranslationService } from './translation.service'; import { JurisdictionService } from './jurisdiction.service'; import { Jurisdiction } from '../dtos/jurisdictions/jurisdiction.dto'; -import { LanguagesEnum, ReviewOrderTypeEnum } from '@prisma/client'; +import { + LanguagesEnum, + ListingEventsTypeEnum, + ReviewOrderTypeEnum, +} from '@prisma/client'; import { IdDTO } from '../dtos/shared/id.dto'; import { Listing } from '../dtos/listings/listing.dto'; import { SendGridService } from './sendgrid.service'; import { ApplicationCreate } from '../dtos/applications/application-create.dto'; import { User } from '../dtos/users/user.dto'; +import Unit from '../dtos/units/unit.dto'; dayjs.extend(utc); dayjs.extend(tz); dayjs.extend(advanced); @@ -40,8 +49,10 @@ export class EmailService { constructor( private readonly sendGrid: SendGridService, + private readonly configService: ConfigService, private readonly translationService: TranslationService, private readonly jurisdictionService: JurisdictionService, + private readonly httpService: HttpService, ) { this.polyglot = new Polyglot({ phrases: {}, @@ -95,6 +106,42 @@ export class EmailService { return partials; } + async govSend(rawHtml: string, subject: string) { + const { + GOVDELIVERY_API_URL, + GOVDELIVERY_USERNAME, + GOVDELIVERY_PASSWORD, + GOVDELIVERY_TOPIC, + } = process.env; + const isGovConfigured = + !!GOVDELIVERY_API_URL && + !!GOVDELIVERY_USERNAME && + !!GOVDELIVERY_PASSWORD && + !!GOVDELIVERY_TOPIC; + if (!isGovConfigured) { + console.warn( + 'failed to configure Govdelivery, ensure that all env variables are provided', + ); + return; + } + + // juice inlines css to allow for email styling + const inlineHtml = juice(rawHtml); + const govEmailXml = `\n ${subject}\n \n \n false\n true\n true\n true\n \n \n ${GOVDELIVERY_TOPIC}\n \n \n \n `; + + await firstValueFrom( + this.httpService.post(GOVDELIVERY_API_URL, govEmailXml, { + headers: { + 'Content-Type': 'application/xml', + Authorization: `Basic ${Buffer.from( + `${GOVDELIVERY_USERNAME}:${GOVDELIVERY_PASSWORD}`, + ).toString('base64')}`, + }, + }), + ); + } + private async send( to: string | string[], from: string, @@ -531,8 +578,188 @@ export class EmailService { ); } + public async listingOpportunity(listing: Listing) { + const jurisdiction = await this.getJurisdiction([listing.jurisdictions]); + void (await this.loadTranslations(jurisdiction, LanguagesEnum.en)); + const compiledTemplate = this.template('listing-opportunity'); + + if (this.configService.get('NODE_ENV') == 'production') { + Logger.log( + `Preparing to send a listing opportunity email for ${listing.name} from ${jurisdiction.emailFromAddress}...`, + ); + } + + // Gather all variables from each unit into one place + const units: { + bedrooms: { [key: number]: Unit[] }; + rent: number[]; + minIncome: number[]; + maxIncome: number[]; + } = listing.units?.reduce( + (summaries, unit) => { + if (unit.monthlyIncomeMin) { + summaries.minIncome.push(Number.parseFloat(unit.monthlyIncomeMin)); + } + if (unit.annualIncomeMax) { + summaries.maxIncome.push( + Number.parseFloat(unit.annualIncomeMax) / 12.0, + ); + } + if (unit.monthlyRent) { + summaries.rent.push(Number.parseFloat(unit.monthlyRent)); + } + const thisBedroomInfo = summaries.bedrooms[unit.unitTypes?.name]; + summaries.bedrooms[unit.unitTypes?.name] = thisBedroomInfo + ? [...thisBedroomInfo, unit] + : [unit]; + return summaries; + }, + { + bedrooms: {}, + rent: [], + minIncome: [], + maxIncome: [], + }, + ); + const tableRows = []; + if (listing.reservedCommunityTypes?.name) { + tableRows.push({ + label: this.polyglot.t('rentalOpportunity.community'), + value: this.formatCommunityType[listing.reservedCommunityTypes.name], + }); + } + if (listing.applicationDueDate) { + tableRows.push({ + label: this.polyglot.t('rentalOpportunity.applicationsDue'), + value: dayjs(listing.applicationDueDate).format('MMMM D, YYYY'), + }); + } + tableRows.push({ + label: this.polyglot.t('rentalOpportunity.address'), + value: `${listing.listingsBuildingAddress.street}, ${listing.listingsBuildingAddress.city} ${listing.listingsBuildingAddress.state} ${listing.listingsBuildingAddress.zipCode}`, + }); + Object.entries(units.bedrooms).forEach(([key, bedroom]) => { + const sqFtString = this.formatUnitDetails(bedroom, 'sqFeet', 'sqft'); + const bathroomstring = this.formatUnitDetails( + bedroom, + 'numBathrooms', + 'bath', + 'baths', + ); + tableRows.push({ + label: this.polyglot.t(`rentalOpportunity.${key}`), + value: `${bedroom.length} unit${ + bedroom.length > 1 ? 's' : '' + }${bathroomstring}${sqFtString}`, + }); + }); + if (units.rent?.length) { + tableRows.push({ + label: this.polyglot.t('rentalOpportunity.rent'), + value: this.formatPricing(units.rent), + }); + } + if (units.minIncome?.length) { + tableRows.push({ + label: this.polyglot.t('rentalOpportunity.minIncome'), + value: this.formatPricing(units.minIncome), + }); + } + if (units.maxIncome?.length) { + tableRows.push({ + label: this.polyglot.t('rentalOpportunity.maxIncome'), + value: this.formatPricing(units.maxIncome), + }); + } + if (listing.listingEvents && listing.listingEvents.length > 0) { + const lotteryEvent = listing.listingEvents.find( + (event) => event.type === ListingEventsTypeEnum.publicLottery, + ); + if (lotteryEvent && lotteryEvent.startDate) { + tableRows.push({ + label: this.polyglot.t('rentalOpportunity.lottery'), + value: dayjs(lotteryEvent.startDate).format('MMMM D, YYYY'), + }); + } + } + + const languages = [ + { + name: this.polyglot.t('rentalOpportunity.viewButton.en'), + code: LanguagesEnum.en, + }, + { + name: this.polyglot.t('rentalOpportunity.viewButton.es'), + code: LanguagesEnum.es, + }, + { + name: this.polyglot.t('rentalOpportunity.viewButton.zh'), + code: LanguagesEnum.zh, + }, + { + name: this.polyglot.t('rentalOpportunity.viewButton.vi'), + code: LanguagesEnum.vi, + }, + { + name: this.polyglot.t('rentalOpportunity.viewButton.tl'), + code: LanguagesEnum.tl, + }, + ]; + + const languageUrls = languages.map((language) => { + return { + name: language.name, + url: `${jurisdiction.publicUrl}/${language.code}/listing/${listing.id}`, + }; + }); + + const compiled = compiledTemplate({ + listingName: listing.name, + tableRows, + languageUrls, + }); + + await this.govSend(compiled, 'New rental opportunity'); + } + formatLocalDate(rawDate: string | Date, format: string): string { const utcDate = dayjs.utc(rawDate); return utcDate.format(format); } + + formatPricing = (values: number[]): string => { + const minPrice = Math.min(...values); + const maxPrice = Math.max(...values); + return `$${minPrice.toLocaleString()}${ + minPrice !== maxPrice ? ' - $' + maxPrice.toLocaleString() : '' + } per month`; + }; + + formatUnitDetails = ( + units: Unit[], + field: string, + label: string, + pluralLabel?: string, + ): string => { + const mappedField = units.reduce((values, unit) => { + if (unit[field]) { + values.push(Number.parseFloat(unit[field])); + } + return values; + }, []); + if (mappedField?.length) { + const minValue = Math.min(...mappedField); + const maxValue = Math.max(...mappedField); + return `, ${minValue.toLocaleString()}${ + minValue !== maxValue ? ' - ' + maxValue.toLocaleString() : '' + } ${pluralLabel && maxValue === 1 ? pluralLabel : label}`; + } + return ''; + }; + + formatCommunityType = { + senior55: 'Seniors 55+', + senior62: 'Seniors 62+', + specialNeeds: 'Special Needs', + }; } diff --git a/api/src/services/listing.service.ts b/api/src/services/listing.service.ts index 02125c66405..beb4e1d0a95 100644 --- a/api/src/services/listing.service.ts +++ b/api/src/services/listing.service.ts @@ -1026,6 +1026,23 @@ export class ListingService implements OnModuleInit { }); } await this.cachePurge(undefined, dto.status, rawListing.id); + if (rawListing.status === ListingsStatusEnum.active) { + // The email send to gov delivery should not be a blocker from the normal flow so wrapping this in a try catch + try { + const jurisdiction = await this.prisma.jurisdictions.findFirst({ + where: { + id: rawListing.jurisdictions?.id, + }, + }); + if (jurisdiction.enableListingOpportunity) { + await this.emailService.listingOpportunity( + rawListing as unknown as Listing, + ); + } + } catch (error) { + console.error(`Error: unable to send to govDelivery ${error}`); + } + } return mapTo(Listing, rawListing); } @@ -1528,6 +1545,26 @@ export class ListingService implements OnModuleInit { await this.cachePurge(storedListing.status, dto.status, rawListing.id); + if ( + dto.status === ListingsStatusEnum.active && + storedListing.status !== ListingsStatusEnum.active + ) { + // The email send to gov delivery should not be a blocker from the normal flow so wrapping this in a try catch + try { + const jurisdiction = await this.prisma.jurisdictions.findFirst({ + where: { + id: rawListing.jurisdictions?.id, + }, + }); + if (jurisdiction.enableListingOpportunity) { + await this.emailService.listingOpportunity( + rawListing as unknown as Listing, + ); + } + } catch (error) { + console.error(`Error: unable to send to govDelivery ${error}`); + } + } return mapTo(Listing, rawListing); } diff --git a/api/src/services/user.service.ts b/api/src/services/user.service.ts index 69bcb05bdc2..c2012946de0 100644 --- a/api/src/services/user.service.ts +++ b/api/src/services/user.service.ts @@ -119,7 +119,7 @@ export class UserService { async update( dto: UserUpdate, requestingUser: User, - jurisdictionName: string, + jurisdictionName?: string, ): Promise { const storedUser = await this.findUserOrError({ userId: dto.id }, true); diff --git a/api/src/views/listing-opportunity.hbs b/api/src/views/listing-opportunity.hbs new file mode 100644 index 00000000000..d442ff45f73 --- /dev/null +++ b/api/src/views/listing-opportunity.hbs @@ -0,0 +1,38 @@ +{{#> layout_default }} + +

{{t "rentalOpportunity.intro"}}
{{listingName}}

+ + + + + + + + + + + {{#each languageUrls}} + + + + {{/each}} + + +{{/layout_default }} diff --git a/api/test/integration/listing.e2e-spec.ts b/api/test/integration/listing.e2e-spec.ts index c7314e3baaf..b570e17a801 100644 --- a/api/test/integration/listing.e2e-spec.ts +++ b/api/test/integration/listing.e2e-spec.ts @@ -57,10 +57,15 @@ describe('Listing Controller Tests', () => { requestApproval: async () => {}, changesRequested: async () => {}, listingApproved: async () => {}, + listingOpportunity: async () => {}, }; const mockChangesRequested = jest.spyOn(testEmailService, 'changesRequested'); const mockRequestApproval = jest.spyOn(testEmailService, 'requestApproval'); const mockListingApproved = jest.spyOn(testEmailService, 'listingApproved'); + const mockListingOpportunity = jest.spyOn( + testEmailService, + 'listingOpportunity', + ); beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ @@ -841,8 +846,15 @@ describe('Listing Controller Tests', () => { }); it('update status to listing approved and notify appropriate users', async () => { + // turn on enableListingOpportunity + jurisdictionA.enableListingOpportunity = true; const val = await constructFullListingData(listing.id, jurisdictionA.id); val.status = ListingsStatusEnum.active; + await request(app.getHttpServer()) + .put(`/jurisdictions/${jurisdictionA.id}`) + .send(jurisdictionA) + .set('Cookie', adminAccessToken) + .expect(200); const putApprovedResponse = await request(app.getHttpServer()) .put(`/listings/${listing.id}`) .send(val) @@ -864,6 +876,19 @@ describe('Listing Controller Tests', () => { expect.arrayContaining([partnerUser.email]), jurisdictionA.publicUrl, ); + expect(mockListingOpportunity).toBeCalledWith( + expect.objectContaining({ + id: listing.id, + name: val.name, + }), + ); + // re-disable listing opportunity + jurisdictionA.enableListingOpportunity = false; + await request(app.getHttpServer()) + .put(`/jurisdictions/${jurisdictionA.id}`) + .send(jurisdictionA) + .set('Cookie', adminAccessToken) + .expect(200); }); it('update status to changes requested and notify appropriate users', async () => { diff --git a/api/test/unit/services/email.service.spec.ts b/api/test/unit/services/email.service.spec.ts index a459c1b77ec..c2c417450ed 100644 --- a/api/test/unit/services/email.service.spec.ts +++ b/api/test/unit/services/email.service.spec.ts @@ -16,6 +16,8 @@ import { whiteHouse } from '../../../prisma/seed-helpers/address-factory'; import { Application } from '../../../src/dtos/applications/application.dto'; import { User } from '../../../src/dtos/users/user.dto'; import { ApplicationCreate } from '../../../src/dtos/applications/application-create.dto'; +import { of } from 'rxjs'; +import { HttpService } from '@nestjs/axios'; let sendMock; const translationServiceMock = { @@ -29,6 +31,14 @@ const jurisdictionServiceMock = { return { name: 'Jurisdiction 1' }; }, }; +const httpServiceMock = { + request: jest.fn().mockReturnValue( + of({ + status: 200, + statusText: 'OK', + }), + ), +}; describe('Testing email service', () => { let service: EmailService; @@ -50,6 +60,7 @@ describe('Testing email service', () => { provide: JurisdictionService, useValue: jurisdictionServiceMock, }, + { provide: HttpService, useValue: httpServiceMock }, GoogleTranslateService, ], }).compile(); diff --git a/api/test/unit/services/user.service.spec.ts b/api/test/unit/services/user.service.spec.ts index 1f09919a400..f302a0554ae 100644 --- a/api/test/unit/services/user.service.spec.ts +++ b/api/test/unit/services/user.service.spec.ts @@ -1,3 +1,4 @@ +import { HttpService } from '@nestjs/axios'; import { Test, TestingModule } from '@nestjs/testing'; import { ConfigService } from '@nestjs/config'; import { PrismaService } from '../../../src/services/prisma.service'; @@ -16,6 +17,7 @@ import { SendGridService } from '../../../src/services/sendgrid.service'; import { User } from '../../../src/dtos/users/user.dto'; import { PermissionService } from '../../../src/services/permission.service'; import { permissionActions } from '../../../src/enums/permissions/permission-actions-enum'; +import { of } from 'rxjs'; describe('Testing user service', () => { let service: UserService; @@ -69,6 +71,14 @@ describe('Testing user service', () => { isConfigured: () => true, fetch: jest.fn(), }; + const httpServiceMock = { + request: jest.fn().mockReturnValue( + of({ + status: 200, + statusText: 'OK', + }), + ), + }; const canOrThrowMock = jest.fn(); @@ -76,12 +86,17 @@ describe('Testing user service', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ UserService, + HttpService, PrismaService, EmailService, ConfigService, SendGridService, TranslationService, JurisdictionService, + { + provide: HttpService, + useValue: httpServiceMock, + }, { provide: SendGridService, useValue: SendGridServiceMock, diff --git a/api/yarn.lock b/api/yarn.lock index 82c117d7f34..1da968078e9 100644 --- a/api/yarn.lock +++ b/api/yarn.lock @@ -3210,7 +3210,7 @@ ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ansi-colors@4.1.3: +ansi-colors@4.1.3, ansi-colors@^4.1.1: version "4.1.3" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== @@ -3561,6 +3561,11 @@ body-parser@1.20.2: type-is "~1.6.18" unpipe "1.0.0" +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + bowser@^2.11.0: version "2.11.0" resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" @@ -3778,6 +3783,31 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +cheerio-select@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" + integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== + dependencies: + boolbase "^1.0.0" + css-select "^5.1.0" + css-what "^6.1.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + +cheerio@^1.0.0-rc.12: + version "1.0.0-rc.12" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683" + integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== + dependencies: + cheerio-select "^2.1.0" + dom-serializer "^2.0.0" + domhandler "^5.0.3" + domutils "^3.0.1" + htmlparser2 "^8.0.1" + parse5 "^7.0.0" + parse5-htmlparser2-tree-adapter "^7.0.0" + chokidar@3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" @@ -3948,6 +3978,11 @@ commander@^2.20.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + comment-json@4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/comment-json/-/comment-json-4.2.3.tgz#50b487ebbf43abe44431f575ebda07d30d015365" @@ -4127,6 +4162,22 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + +css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + cssom@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" @@ -4301,6 +4352,29 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-serializer@^1.0.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" + integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + domexception@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" @@ -4308,6 +4382,45 @@ domexception@^4.0.0: dependencies: webidl-conversions "^7.0.0" +domhandler@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-3.3.0.tgz#6db7ea46e4617eb15cf875df68b2b8524ce0037a" + integrity sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA== + dependencies: + domelementtype "^2.0.1" + +domhandler@^4.2.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== + dependencies: + domelementtype "^2.2.0" + +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + +domutils@^2.4.2: + version "2.8.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + +domutils@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + dotenv-expand@10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37" @@ -4408,7 +4521,12 @@ ent@^2.2.0: resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" integrity sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA== -entities@^4.4.0: +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +entities@^4.2.0, entities@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== @@ -4508,6 +4626,11 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escape-goat@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-3.0.0.tgz#e8b5fb658553fe8a3c4959c316c6ebb8c842b19c" + integrity sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw== + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -5402,6 +5525,26 @@ html-tags@^3.0.0: resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== +htmlparser2@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-5.0.1.tgz#7daa6fc3e35d6107ac95a4fc08781f091664f6e7" + integrity sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ== + dependencies: + domelementtype "^2.0.1" + domhandler "^3.3.0" + domutils "^2.4.2" + entities "^2.0.0" + +htmlparser2@^8.0.1: + version "8.0.2" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" + integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + entities "^4.4.0" + http-errors@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" @@ -6485,6 +6628,17 @@ jsonwebtoken@~9.0.1: ms "^2.1.1" semver "^7.5.4" +juice@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/juice/-/juice-9.1.0.tgz#3ef8a12392d44c1cd996022aa977581049a65050" + integrity sha512-odblShmPrUoHUwRuC8EmLji5bPP2MLO1GL+gt4XU3tT2ECmbSrrMjtMQaqg3wgMFP2zvUzdPZGfxc5Trk3Z+fQ== + dependencies: + cheerio "^1.0.0-rc.12" + commander "^6.1.0" + mensch "^0.3.4" + slick "^1.12.2" + web-resource-inliner "^6.0.1" + just-extend@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-6.2.0.tgz#b816abfb3d67ee860482e7401564672558163947" @@ -6773,6 +6927,11 @@ memfs@^3.4.1: dependencies: fs-monkey "^1.0.3" +mensch@^0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/mensch/-/mensch-0.3.4.tgz#770f91b46cb16ea5b204ee735768c3f0c491fecd" + integrity sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g== + merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -6818,7 +6977,7 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@2.6.0: +mime@2.6.0, mime@^2.4.6: version "2.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== @@ -6982,6 +7141,13 @@ node-emoji@1.11.0: dependencies: lodash "^4.17.21" +node-fetch@^2.6.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-fetch@^2.6.1: version "2.6.9" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" @@ -7044,6 +7210,13 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + nwsapi@^2.2.2: version "2.2.7" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.7.tgz#738e0707d3128cb750dddcfe90e4610482df0f30" @@ -7208,6 +7381,14 @@ parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parse5-htmlparser2-tree-adapter@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1" + integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g== + dependencies: + domhandler "^5.0.2" + parse5 "^7.0.0" + parse5@^7.0.0, parse5@^7.1.1: version "7.1.2" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" @@ -7919,6 +8100,11 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slick@^1.12.2: + version "1.12.2" + resolved "https://registry.yarnpkg.com/slick/-/slick-1.12.2.tgz#bd048ddb74de7d1ca6915faa4a57570b3550c2d7" + integrity sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A== + source-map-support@0.5.13: version "0.5.13" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" @@ -8628,6 +8814,11 @@ v8-to-istanbul@^9.0.1: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" +valid-data-url@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/valid-data-url/-/valid-data-url-3.0.1.tgz#826c1744e71b5632e847dd15dbd45b9fb38aa34f" + integrity sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA== + validator@^13.7.0: version "13.9.0" resolved "https://registry.yarnpkg.com/validator/-/validator-13.9.0.tgz#33e7b85b604f3bbce9bb1a05d5c3e22e1c2ff855" @@ -8674,6 +8865,18 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +web-resource-inliner@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz#df0822f0a12028805fe80719ed52ab6526886e02" + integrity sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A== + dependencies: + ansi-colors "^4.1.1" + escape-goat "^3.0.0" + htmlparser2 "^5.0.0" + mime "^2.4.6" + node-fetch "^2.6.0" + valid-data-url "^3.0.0" + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" diff --git a/tasks/import-listings/package.json b/tasks/import-listings/package.json index 840def43268..3755d1d741a 100644 --- a/tasks/import-listings/package.json +++ b/tasks/import-listings/package.json @@ -9,7 +9,7 @@ "test": "jest --runInBand --detectOpenHandles", "test:cov": "jest --coverage", "import:run": "node ./.build/index.js", - "import:run:local": "DATABASE_URL=\"${DATABASE_URL:-postgres://localhost/bloom}\" JURISDICTION_INCLUDE_LIST=\"${JURISDICTION_INCLUDE_LIST:-San Jose,San Mateo,Alameda}\" yarn import:run", + "import:run:local": "DATABASE_URL=\"${DATABASE_URL:-postgres://localhost/bloom_prisma}\" JURISDICTION_INCLUDE_LIST=\"${JURISDICTION_INCLUDE_LIST:-San Jose,San Mateo,Alameda}\" yarn import:run", "import:run:local:dev": "EXTERNAL_API_BASE=https://api.housingbayarea.bloom.exygy.dev LISTING_VIEW=full yarn import:run:local", "import:run:local:prod": "EXTERNAL_API_BASE=https://proxy.housingbayarea.org yarn import:run:local" },