Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: updates to the styling for Seeds Buttons and Common App #442

Merged
merged 17 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Bloom consists of a client/server architecture using [Next.js](https://nextjs.or

The frontend apps can easily be deployed to any Jamstack-friendly web host such as [Netlify](https://www.netlify.com/) or Vercel. The frontend build process performs a static rendering of as much of the React page component trees as possible based on API data available at the time of the build. Additional real-time interactivity is made possible by React components at run-time.

The backend can be simultaenously deployed to PaaS-style hosts such as Heroku. Its primary architectural dependency is a PostgreSQL database.
The backend can be simultaneously deployed to PaaS-style hosts such as Heroku. Its primary architectural dependency is a PostgreSQL database.

### Structure

Expand Down
15 changes: 9 additions & 6 deletions backend/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"@anchan828/nest-sendgrid": "^0.3.25",
"@aws-sdk/client-s3": "^3.326.0",
"@aws-sdk/s3-request-presigner": "^3.327.0",
"@google-cloud/translate": "^6.2.6",
"@google-cloud/translate": "^7.2.1",
"@nestjs/axios": "^2.0.0",
"@nestjs/cli": "^9.5.0",
"@nestjs/common": "^9.4.2",
Expand All @@ -60,9 +60,12 @@
"@nestjs/swagger": "^6.3.0",
"@nestjs/throttler": "^4.0.0",
"@nestjs/typeorm": "^9.0.1",
"@turf/buffer": "6.5.0",
"@turf/helpers": "6.5.0",
"@turf/boolean-point-in-polygon": "6.5.0",
"@types/cache-manager": "^3.4.0",
"async-retry": "^1.3.1",
"axios": "0.21.2",
"axios": "0.21.3",
"cache-manager": "^3.4.0",
"casbin": "5.13.0",
"class-transformer": "0.3.1",
Expand All @@ -82,17 +85,17 @@
"nanoid": "^3.1.12",
"nestjs-twilio": "^4.1.1",
"nestjs-typeorm-paginate": "^4.0.3",
"newrelic": "7.5.1",
"newrelic": "11.4.0",
"node-polyglot": "^2.4.0",
"passport": "^0.6.0",
"passport-custom": "^1.1.1",
"passport-jwt": "^4.0.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"pg": "^8.4.1",
"pg": "^8.11.3",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.5.4",
"swagger-ui-express": "^4.1.4",
"swagger-ui-express": "^4.2.0",
"ts-node": "10.8.0",
"twilio": "^3.71.3",
"typeorm": "0.3.12",
Expand Down
3 changes: 2 additions & 1 deletion backend/core/src/applications/applications.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { CsvBuilder } from "./services/csv-builder.service"
import { ApplicationCsvExporterService } from "./services/application-csv-exporter.service"
import { EmailModule } from "../email/email.module"
import { ActivityLogModule } from "../activity-log/activity-log.module"
import { GeocodingService } from "./services/geocoding.service"

@Module({
imports: [
Expand All @@ -28,7 +29,7 @@ import { ActivityLogModule } from "../activity-log/activity-log.module"
EmailModule,
ScheduleModule.forRoot(),
],
providers: [ApplicationsService, CsvBuilder, ApplicationCsvExporterService],
providers: [ApplicationsService, CsvBuilder, ApplicationCsvExporterService, GeocodingService],
exports: [ApplicationsService],
controllers: [ApplicationsController, ApplicationsSubmissionController],
})
Expand Down
11 changes: 11 additions & 0 deletions backend/core/src/applications/services/applications.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { Listing } from "../../listings/entities/listing.entity"
import { ApplicationCsvExporterService } from "./application-csv-exporter.service"
import { User } from "../../auth/entities/user.entity"
import { StatusDto } from "../../shared/dto/status.dto"
import { GeocodingService } from "./geocoding.service"

@Injectable({ scope: Scope.REQUEST })
export class ApplicationsService {
Expand All @@ -38,6 +39,7 @@ export class ApplicationsService {
private readonly listingsService: ListingsService,
private readonly emailService: EmailService,
private readonly applicationCsvExporter: ApplicationCsvExporterService,
private readonly geocodingService: GeocodingService,
@InjectRepository(Application) private readonly repository: Repository<Application>,
@InjectRepository(Listing) private readonly listingsRepository: Repository<Listing>
) {}
Expand Down Expand Up @@ -421,6 +423,15 @@ export class ApplicationsService {
if (application.applicant.emailAddress && shouldSendConfirmation) {
await this.emailService.confirmation(listing, application, applicationCreateDto.appUrl)
}

// Calculate geocoding preferences after save and email sent
if (listing.jurisdiction?.enableGeocodingPreferences) {
try {
void this.geocodingService.validateGeocodingPreferences(application, listing)
} catch (e) {
console.warn("error while validating geocoding preferences")
}
}
return application
}

Expand Down
167 changes: 167 additions & 0 deletions backend/core/src/applications/services/geocoding.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { Test, TestingModule } from "@nestjs/testing"
import { GeocodingService } from "./geocoding.service"
import { Address } from "../../shared/entities/address.entity"
import { getRepositoryToken } from "@nestjs/typeorm"
import { Application } from "../entities/application.entity"
import { ValidationMethod } from "../../multiselect-question/types/validation-method-enum"
import { Listing } from "../../listings/entities/listing.entity"
import { InputType } from "../../shared/types/input-type"

describe("GeocodingService", () => {
let service: GeocodingService
const applicationRepoUpdate = jest.fn()
const mockApplicationRepo = {
createQueryBuilder: jest.fn(),
update: applicationRepoUpdate,
}
const date = new Date()
const listingAddress: Address = {
id: "id",
createdAt: date,
updatedAt: date,
city: "Washington",
county: null,
state: "DC",
street: "1600 Pennsylvania Avenue",
street2: null,
zipCode: "20500",
latitude: 38.8977,
longitude: -77.0365,
}

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
GeocodingService,
{
provide: getRepositoryToken(Application),
useValue: mockApplicationRepo,
},
],
}).compile()

service = await module.resolve(GeocodingService)
})

describe("verifyRadius", () => {
it("should return 'unknown' if lat and long not there", () => {
expect(
service.verifyRadius(
{
...listingAddress,
latitude: null,
longitude: null,
},
5,
listingAddress
)
).toBe("unknown")
})
it("should return 'true' if within radius", () => {
expect(
service.verifyRadius(
{
...listingAddress,
latitude: 38.89485,
longitude: -77.04251,
},
5,
listingAddress
)
).toBe("true")
})
it("should return 'false' if not within radius", () => {
expect(
service.verifyRadius(
{
...listingAddress,
latitude: 39.284205,
longitude: -76.621698,
},
5,
listingAddress
)
).toBe("false")
})
it("should return 'true' if same lat long", () => {
expect(
service.verifyRadius(
{
...listingAddress,
},
5,
listingAddress
)
).toBe("true")
})
})
describe("validateRadiusPreferences", () => {
const listing = {
buildingAddress: listingAddress,
listingMultiselectQuestions: [
{
multiselectQuestion: {
options: [
{
text: "Geocoding option by radius",
collectAddress: true,
radiusSize: 5,
validationMethod: ValidationMethod.radius,
},
],
},
},
],
}
const preferenceAddress = { ...listingAddress, latitude: 38.89485, longitude: -77.04251 }
const application = {
id: "applicationId",
preferences: [
{
key: "Geocoding preference",
options: [
{
key: "Geocoding option by radius",
checked: true,
extraData: [
{
type: InputType.address,
value: preferenceAddress,
},
],
},
],
},
],
}
it("should save the validated value as extraData", async () => {
await service.validateRadiusPreferences(
(application as unknown) as Application,
listing as Listing
)
expect(applicationRepoUpdate).toBeCalledWith(
{ id: "applicationId" },
{
preferences: expect.arrayContaining([
expect.objectContaining({
key: "Geocoding preference",
options: [
{
checked: true,
extraData: [
{
type: "address",
value: preferenceAddress,
},
{ key: "geocodingVerified", type: "text", value: "true" },
],
key: "Geocoding option by radius",
},
],
}),
]),
}
)
})
})
})
101 changes: 101 additions & 0 deletions backend/core/src/applications/services/geocoding.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { point } from "@turf/helpers"
import buffer from "@turf/buffer"
import booleanPointInPolygon from "@turf/boolean-point-in-polygon"
import { InjectRepository } from "@nestjs/typeorm"
import { Repository } from "typeorm"
import { Address } from "../../shared/entities/address.entity"
import { Application } from "../entities/application.entity"
import { Listing } from "../../listings/entities/listing.entity"
import { ValidationMethod } from "../../multiselect-question/types/validation-method-enum"
import { MultiselectOption } from "../../multiselect-question/types/multiselect-option"
import { ApplicationMultiselectQuestion } from "../entities/application-multiselect-question.entity"
import { ApplicationMultiselectQuestionOption } from "../types/application-multiselect-question-option"
import { InputType } from "../../shared/types/input-type"
import { GeocodingValues } from "../../shared/types/geocoding-values"

export class GeocodingService {
constructor(
@InjectRepository(Application) private readonly repository: Repository<Application>
) {}

public async validateGeocodingPreferences(application: Application, listing: Listing) {
await this.validateRadiusPreferences(application, listing)
}

verifyRadius(
preferenceAddress: Address,
radius: number,
listingAddress: Address
): GeocodingValues {
try {
if (preferenceAddress.latitude && preferenceAddress.longitude) {
const preferencePoint = point([
Number.parseFloat(preferenceAddress.longitude.toString()),
Number.parseFloat(preferenceAddress.latitude.toString()),
])
const listingPoint = point([
Number.parseFloat(listingAddress.longitude.toString()),
Number.parseFloat(listingAddress.latitude.toString()),
])
const calculatedBuffer = buffer(listingPoint.geometry, radius, { units: "miles" })
return booleanPointInPolygon(preferencePoint, calculatedBuffer)
? GeocodingValues.true
: GeocodingValues.false
}
} catch (e) {
console.log("error happened while calculating radius")
}
return GeocodingValues.unknown
}

public async validateRadiusPreferences(application: Application, listing: Listing) {
// Get all radius preferences from the listing
const radiusPreferenceOptions: MultiselectOption[] = listing.listingMultiselectQuestions.reduce(
(options, multiselectQuestion) => {
const newOptions = multiselectQuestion.multiselectQuestion.options?.filter(
(option) => option.validationMethod === ValidationMethod.radius
)
return [...options, ...newOptions]
},
[]
)
// If there are any radius preferences do the calculation and save the new preferences
if (radiusPreferenceOptions.length) {
const preferences: ApplicationMultiselectQuestion[] = application.preferences.map(
(preference) => {
const newPreferenceOptions: ApplicationMultiselectQuestionOption[] = preference.options.map(
(option) => {
const addressData = option.extraData.find((data) => data.type === InputType.address)
if (option.checked && addressData) {
const foundOption = radiusPreferenceOptions.find(
(preferenceOption) => preferenceOption.text === option.key
)
if (foundOption) {
const geocodingVerified = this.verifyRadius(
addressData.value as Address,
foundOption.radiusSize,
listing.buildingAddress
)
return {
...option,
extraData: [
...option.extraData,
{
key: "geocodingVerified",
type: InputType.text,
value: geocodingVerified,
},
],
}
}
}
return option
}
)
return { ...preference, options: newPreferenceOptions }
}
)
await this.repository.update({ id: application.id }, { preferences: preferences })
}
}
}
5 changes: 5 additions & 0 deletions backend/core/src/shared/types/geocoding-values.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum GeocodingValues {
true = "true",
false = "false",
unknown = "unknown",
}
Loading
Loading