Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a473b7e
feat: application create and read refactor
mcgarrye Dec 16, 2025
33ca73e
Merge branch 'main' into 5147/msq-refactor-application-service
mcgarrye Dec 17, 2025
2597cb5
Merge branch 'main' into 5147/msq-refactor-application-service
mcgarrye Dec 17, 2025
54eb283
feat: update tests with new fields
mcgarrye Dec 17, 2025
e69531b
feat: fix typing error
mcgarrye Dec 17, 2025
3e6c7da
Merge branch 'main' into 5147/msq-refactor-application-service
mcgarrye Dec 22, 2025
b4c0f71
Merge branch 'main' into 5147/msq-refactor-application-service
mcgarrye Dec 23, 2025
1837c52
fix: flakey test
mcgarrye Dec 23, 2025
cdae353
fix: adjust flaky test
mcgarrye Dec 23, 2025
a12e534
fix: toContainEqual
mcgarrye Dec 23, 2025
cffa9bc
feat: geocoding refactor
mcgarrye Dec 23, 2025
42fbfbf
Merge branch 'main' into 5147/msq-refactor-application-service
mcgarrye Jan 2, 2026
52abbeb
feat: follow export standards
mcgarrye Jan 2, 2026
8bdb897
Merge branch 'main' into 5147/msq-refactor-application-service
mcgarrye Jan 6, 2026
8a3996b
Merge branch '5147/msq-refactor-application-service' into 5705/msq-re…
mcgarrye Jan 6, 2026
2d881d7
feat: false flow testing
mcgarrye Jan 6, 2026
2797d4f
Merge branch 'main' into 5705/msq-refactor-geocoding-service
mcgarrye Jan 6, 2026
abaeb97
feat: adjust imports
mcgarrye Jan 6, 2026
1dfccbb
Merge branch 'main' into 5705/msq-refactor-geocoding-service
mcgarrye Jan 6, 2026
174cc07
Merge branch 'main' into 5705/msq-refactor-geocoding-service
mcgarrye Jan 6, 2026
d2138a6
Merge branch 'main' into 5705/msq-refactor-geocoding-service
mcgarrye Jan 7, 2026
4b338df
Merge branch 'main' into 5705/msq-refactor-geocoding-service
mcgarrye Jan 8, 2026
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
1 change: 1 addition & 0 deletions api/prisma/seed-helpers/multiselect-question-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,6 @@ const multiselectOptionFactoryV2 = (numberToMake: number) => {
return [...new Array(numberToMake)].map((_, index) => ({
name: randomNoun(),
ordinal: index,
shouldCollectAddress: index % 2 === 0,
}));
};
36 changes: 27 additions & 9 deletions api/src/services/application.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,9 @@ export class ApplicationService {
// address is needed for geocoding
listingsBuildingAddress: true,
listingMultiselectQuestions: {
include: { multiselectQuestions: true },
include: {
multiselectQuestions: { include: { multiselectOptions: true } },
},
},
// support unit group availability logic in email
unitGroups: true,
Expand Down Expand Up @@ -893,9 +895,10 @@ export class ApplicationService {
rawApplication.applicationSelections = rawSelections;

const mappedApplication = mapTo(Application, rawApplication);
const mappedListing = mapTo(Listing, listing);
if (dto.applicant.emailAddress && forPublic) {
this.emailService.applicationConfirmation(
mapTo(Listing, listing),
mappedListing,
mappedApplication,
listing.jurisdictions?.publicUrl,
);
Expand All @@ -904,13 +907,26 @@ export class ApplicationService {
await this.updateListingApplicationEditTimestamp(listing.id);

// Calculate geocoding preferences after save and email sent
if (!enableV2MSQ && listing.jurisdictions?.enableGeocodingPreferences) {
if (listing.jurisdictions?.enableGeocodingPreferences) {
try {
// TODO: Rewrite for V2MSQ
void this.geocodingService.validateGeocodingPreferences(
mappedApplication,
mapTo(Listing, listing),
);
if (enableV2MSQ) {
const multiselectOptions =
mappedListing.listingMultiselectQuestions.flatMap(
(multiselectQuestion) =>
multiselectQuestion.multiselectQuestions.multiselectOptions,
);

void this.geocodingService.validateGeocodingPreferencesV2(
mappedApplication.applicationSelections,
mappedListing.listingsBuildingAddress,
multiselectOptions,
);
} else {
void this.geocodingService.validateGeocodingPreferences(
mappedApplication,
mappedListing,
);
}
} catch (e) {
// If the geocoding fails it should not prevent the request from completing so
// catching all errors here
Expand Down Expand Up @@ -947,7 +963,9 @@ export class ApplicationService {
include: {
jurisdictions: { include: { featureFlags: true } },
listingMultiselectQuestions: {
include: { multiselectQuestions: true },
include: {
multiselectQuestions: { include: { multiselectOptions: true } },
},
},
},
});
Expand Down
101 changes: 93 additions & 8 deletions api/src/services/geocoding.service.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { FeatureCollection, Polygon, point } from '@turf/helpers';
import { Injectable } from '@nestjs/common';
import { MapLayers, Prisma } from '@prisma/client';
import buffer from '@turf/buffer';
import { FeatureCollection, Polygon, point } from '@turf/helpers';
import pointsWithinPolygon from '@turf/points-within-polygon';
import booleanPointInPolygon from '@turf/boolean-point-in-polygon';
import { MapLayers, Prisma } from '@prisma/client';
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { Address } from '../dtos/addresses/address.dto';
import { Application } from '../dtos/applications/application.dto';
import { ApplicationMultiselectQuestion } from '../dtos/applications/application-multiselect-question.dto';
import { ApplicationMultiselectQuestionOption } from '../dtos/applications/application-multiselect-question-option.dto';
import { ApplicationSelection } from '../dtos/applications/application-selection.dto';
import { ApplicationSelectionOption } from '../dtos/applications/application-selection-option.dto';
import Listing from '../dtos/listings/listing.dto';
import { MultiselectOption } from '../dtos/multiselect-questions/multiselect-option.dto';
import { ApplicationMultiselectQuestion } from '../dtos/applications/application-multiselect-question.dto';
import { PrismaService } from './prisma.service';
import { ValidationMethod } from '../enums/multiselect-questions/validation-method-enum';
import { ApplicationMultiselectQuestionOption } from '../dtos/applications/application-multiselect-question-option.dto';
import { Address } from '../dtos/addresses/address.dto';
import { InputType } from '../enums/shared/input-type-enum';
import pointsWithinPolygon from '@turf/points-within-polygon';

@Injectable()
export class GeocodingService {
Expand All @@ -32,6 +34,89 @@ export class GeocodingService {
});
}

public async validateGeocodingPreferencesV2(
applicationSelections: ApplicationSelection[],
listingsBuildingAddress: Address,
multiselectOptions: MultiselectOption[],
) {
const mapOptions: MultiselectOption[] = multiselectOptions.filter(
(option) =>
option.validationMethod === ValidationMethod.map && option.mapLayerId,
);
const radiusOptions: MultiselectOption[] = multiselectOptions.filter(
(option) => option.validationMethod === ValidationMethod.radius,
);

if (!mapOptions.length && !radiusOptions.length) {
return;
}

let mapOptionIds = [];
let mapLayers = [];
if (mapOptions.length) {
mapOptionIds = mapOptions.map((mapOption) => mapOption.id);
mapLayers = await this.prisma.mapLayers.findMany({
where: {
id: { in: mapOptions.map((option) => option.mapLayerId) },
},
});
}
const radiusOptionIds = radiusOptions.map(
(radiusOption) => radiusOption.id,
);

const selectionOptions: ApplicationSelectionOption[] =
applicationSelections.flatMap((selection) => selection.selections);

for (const selectionOption of selectionOptions) {
const addressData = selectionOption.addressHolderAddress;
if (addressData) {
// Checks if there are any preferences that have a validation method of 'map',
// validates those preferences addresses,
// and then adds the appropriate validation check field to those preferences
if (mapOptionIds.includes(selectionOption.multiselectOption.id)) {
const foundOption = mapOptions.find(
(option) => option.id === selectionOption.multiselectOption.id,
);
const layer = mapLayers.find(
(layer) => layer.id === foundOption.mapLayerId,
);
const geocodingVerified = this.verifyLayers(
addressData,
layer?.featureCollection as unknown as FeatureCollection,
);
await this.prisma.applicationSelectionOptions.update({
data: {
isGeocodingVerified: geocodingVerified,
},
where: { id: selectionOption.id },
});
}
// Checks if there are any preferences that have a validation method of radius,
// validates those preferences addresses,
// and then adds the appropriate validation check field to those preferences
else if (
radiusOptionIds.includes(selectionOption.multiselectOption.id)
) {
const foundOption = radiusOptions.find(
(option) => option.id === selectionOption.multiselectOption.id,
);
const geocodingVerified = this.verifyRadius(
addressData,
foundOption.radiusSize,
listingsBuildingAddress,
);
await this.prisma.applicationSelectionOptions.update({
data: {
isGeocodingVerified: geocodingVerified,
},
where: { id: selectionOption.id },
});
}
}
}
}

verifyRadius(
preferenceAddress: Address,
radius: number,
Expand Down
27 changes: 21 additions & 6 deletions api/test/unit/services/application.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1627,7 +1627,9 @@ describe('Testing application service', () => {
jurisdictions: { include: { featureFlags: true } },
listingsBuildingAddress: true,
listingMultiselectQuestions: {
include: { multiselectQuestions: true },
include: {
multiselectQuestions: { include: { multiselectOptions: true } },
},
},
unitGroups: true,
},
Expand Down Expand Up @@ -1859,7 +1861,9 @@ describe('Testing application service', () => {
jurisdictions: { include: { featureFlags: true } },
listingsBuildingAddress: true,
listingMultiselectQuestions: {
include: { multiselectQuestions: true },
include: {
multiselectQuestions: { include: { multiselectOptions: true } },
},
},
unitGroups: true,
},
Expand Down Expand Up @@ -2080,6 +2084,9 @@ describe('Testing application service', () => {
prisma.applicationSelections.create = jest.fn().mockResolvedValue({
id: randomUUID(),
});
prisma.applicationSelectionOptions.update = jest
.fn()
.mockResolvedValue(null);

const exampleAddress = addressFactory() as AddressCreate;
const dto = mockCreateApplicationData(
Expand All @@ -2102,7 +2109,9 @@ describe('Testing application service', () => {
jurisdictions: { include: { featureFlags: true } },
listingsBuildingAddress: true,
listingMultiselectQuestions: {
include: { multiselectQuestions: true },
include: {
multiselectQuestions: { include: { multiselectOptions: true } },
},
},
unitGroups: true,
},
Expand Down Expand Up @@ -2376,7 +2385,9 @@ describe('Testing application service', () => {
jurisdictions: { include: { featureFlags: true } },
listingsBuildingAddress: true,
listingMultiselectQuestions: {
include: { multiselectQuestions: true },
include: {
multiselectQuestions: { include: { multiselectOptions: true } },
},
},
unitGroups: true,
},
Expand Down Expand Up @@ -2425,7 +2436,9 @@ describe('Testing application service', () => {
jurisdictions: { include: { featureFlags: true } },
listingsBuildingAddress: true,
listingMultiselectQuestions: {
include: { multiselectQuestions: true },
include: {
multiselectQuestions: { include: { multiselectOptions: true } },
},
},
unitGroups: true,
},
Expand Down Expand Up @@ -2480,7 +2493,9 @@ describe('Testing application service', () => {
jurisdictions: { include: { featureFlags: true } },
listingsBuildingAddress: true,
listingMultiselectQuestions: {
include: { multiselectQuestions: true },
include: {
multiselectQuestions: { include: { multiselectOptions: true } },
},
},
unitGroups: true,
},
Expand Down
Loading
Loading