Skip to content

Commit

Permalink
Feat/1369 sort applicants based on rental rules (#50)
Browse files Browse the repository at this point in the history
* feat: 1369 add factories for necessary interfaces

* feat: 1369 tests for assignPriorityToApplicantBasedOnRentalRules

* feat: 1369 write tests for sorting

* feat: 1369 clean

* feat: 1369 remove redundant comments

* feat: 1369 some fixes for CR

* feat: 1368 refactor

* feat: 1369 fix broken test

* feat: 1369 add const for lease.types

* feat: 1369 use includes instead of startsWith
  • Loading branch information
driatic authored May 7, 2024
1 parent 3a6c8e2 commit d928f81
Show file tree
Hide file tree
Showing 8 changed files with 582 additions and 21 deletions.
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"dotenv": "^16.3.2",
"easy-soap-request": "^5.6.1",
"fast-xml-parser": "^4.3.4",
"fishery": "^2.2.2",
"http-errors": "^2.0.0",
"knex": "^2.5.1",
"koa": "^2.15.0",
Expand Down
15 changes: 15 additions & 0 deletions src/constants/leaseTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//constant values for lease.type
//string values taken from xpand

const leaseTypes = {
housingContract: 'Bostadskontrakt',
campusContract: 'Campuskontrakt',
garageContract: 'Garagekontrakt',
cooperativeTenancyContract: 'Kooperativ hyresrätt',
commercialTenantContract: 'Lokalkontrakt',
renegotiationContract: 'Omförhandlingskontrakt',
otherContract: 'Övrigt',
parkingspaceContract: 'P-platskontrakt',
}

export { leaseTypes }
27 changes: 18 additions & 9 deletions src/services/lease-service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,24 @@ import KoaRouter from '@koa/router'
import {
getContactByContactCode,
getContactByNationalRegistrationNumber,
getLeasesForPropertyId,
getContactForPhoneNumber,
getLease,
getLeasesForContactCode,
getLeasesForNationalRegistrationNumber,
getLeasesForPropertyId,
} from './adapters/xpand/tenant-lease-adapter'

import {
createListing,
applicationExists,
createApplication,
createListing,
getAllListingsWithApplicants,
getApplicantById,
getApplicantsByContactCode,
getApplicantsByContactCodeAndRentalObjectCode as getApplicantByContactCodeAndRentalObjectCode,
getListingById,
getListingByRentalObjectCode,
applicationExists,
updateApplicantStatus,
getListingById,
} from './adapters/listing-adapter'
import {
addApplicantToToWaitingList,
Expand All @@ -38,7 +38,11 @@ import {
getInvoicesByContactCode,
getUnpaidInvoicesByContactCode,
} from './adapters/xpand/invoices-adapter'
import { getDetailedApplicantInformation } from './priority-list-service'
import {
addPriorityToApplicantsBasedOnRentalRules,
getDetailedApplicantInformation,
sortApplicantsBasedOnRentalRules,
} from './priority-list-service'
import { Applicant, ApplicantStatus, Listing } from 'onecore-types'

interface CreateLeaseRequest {
Expand Down Expand Up @@ -504,7 +508,7 @@ export const routes = (router: KoaRouter) => {

/**
* Gets detailed information on a listings applicants
* Returns a list of all applicants on a listing by listing id
* Returns a sorted list by rental rules for internal parking spaces of all applicants on a listing by listing id
* Uses ListingId instead of rentalObjectCode since multiple listings can share the same rentalObjectCode for historical reasons
*/
router.get('(.*)/listing/:listingId/applicants/details', async (ctx) => {
Expand All @@ -517,17 +521,22 @@ export const routes = (router: KoaRouter) => {
return
}

const result: any = []
const applicants: any = []

if (listing.applicants) {
for (const applicant of listing.applicants) {
const detailedApplicant =
await getDetailedApplicantInformation(applicant)
result.push(detailedApplicant)
applicants.push(detailedApplicant)
}
}

ctx.body = result
const applicantsWithPriority = addPriorityToApplicantsBasedOnRentalRules(
listing,
applicants
)

ctx.body = sortApplicantsBasedOnRentalRules(applicantsWithPriority)
} catch (error: unknown) {
ctx.status = 500

Expand Down
122 changes: 119 additions & 3 deletions src/services/lease-service/priority-list-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@
import {
Applicant,
Lease,
Listing,
ParkingSpaceApplicationCategory,
parkingSpaceApplicationCategoryTranslation,
WaitingList,
} from 'onecore-types'
import { getWaitingList } from './adapters/xpand/xpand-soap-adapter'
import {
getContactByContactCode,
getResidentialAreaByRentalPropertyId,
getLeasesForContactCode,
getResidentialAreaByRentalPropertyId,
} from './adapters/xpand/tenant-lease-adapter'
import { leaseTypes } from '../../constants/leaseTypes'

const getDetailedApplicantInformation = async (applicant: Applicant) => {
try {
Expand Down Expand Up @@ -89,6 +91,117 @@ const getDetailedApplicantInformation = async (applicant: Applicant) => {
}
}

//todo: should use and return defined interface type
const addPriorityToApplicantsBasedOnRentalRules = (
listing: Listing,
applicants: any[]
) => {
const applicantsWithAssignedPriority: any[] = [] //todo: use defined interface
for (const applicant of applicants) {
applicantsWithAssignedPriority.push(
assignPriorityToApplicantBasedOnRentalRules(listing, applicant)
)
}

return applicantsWithAssignedPriority
}

const sortApplicantsBasedOnRentalRules = (applicants: any[]): any[] => {
return Array.from(applicants).sort((a, b) => {
//sort by priority (ascending)
if (a.priority !== b.priority) {
return a.priority - b.priority
}

//sort by queue points (descending)
return b.queuePoints - a.queuePoints
})
}

//todo: should use and return defined interface type from onecore-types
const assignPriorityToApplicantBasedOnRentalRules = (
listing: Listing,
applicant: any
): any => {
if (applicant.listingId !== listing.id) {
throw new Error(
`applicant ${applicant.contactCode} does not belong to listing ${listing.id}`
)
}

//priority 1

//Applicant has no active parking space contract and is tenant in same area as listing
if (!applicant.parkingSpaceContracts.length) {
if (
applicant.currentHousingContract.residentialArea.code ===
listing.districtCode
) {
return {
...applicant,
priority: 1,
}
}

//Applicant has no active parking space contract and has upcoming housing contract in same area as listing
if (applicant.upcomingHousingContract) {
if (
applicant.upcomingHousingContract.residentialArea.code ===
listing.districtCode
) {
return {
...applicant,
priority: 1,
}
}
}
}

//Applicant has 1 active contract for parking space and wishes to replace current parking space
if (
applicant.parkingSpaceContracts.length === 1 &&
applicant.applicationType === 'Replace'
) {
return {
...applicant,
priority: 1,
}
}

//priority 2

//Applicant has 1 active parking space contract and wishes to rent an additional parking space
if (
applicant.parkingSpaceContracts.length === 1 &&
applicant.applicationType === 'Additional'
) {
return {
...applicant,
priority: 2,
}
}

//Applicant has more than 1 active parking space contract and wishes to replace 1 parking space contract
if (
applicant.parkingSpaceContracts.length > 1 &&
applicant.applicationType === 'Replace'
) {
return {
...applicant,
priority: 2,
}
}

//priority 3

//Applicant has more 2 or more active parking space and wishes to rent an additional parking space

return {
...applicant,
priority: 3,
}
}

//helper function to filter all non-terminated and all still active contracts with a last debit date
const isLeaseActiveOrUpcoming = (lease: Lease): boolean => {
const currentDate = new Date()
Expand Down Expand Up @@ -136,7 +249,7 @@ const parseLeasesForHousingContracts = (
const housingContracts: Lease[] = []
for (const lease of leases) {
//use startsWith to handle whitespace issues from xpand
if (lease.type.startsWith('Bostadskontrakt')) {
if (lease.type.includes(leaseTypes.housingContract)) {
housingContracts.push(lease)
}
}
Expand Down Expand Up @@ -179,7 +292,7 @@ const parseLeasesForParkingSpaces = (leases: Lease[]): Lease[] | undefined => {
const parkingSpaces: Lease[] = []
for (const lease of leases) {
//use startsWith to handle whitespace issues from xpand
if (lease.type.startsWith('P-Platskontrakt')) {
if (lease.type.includes(leaseTypes.parkingspaceContract)) {
parkingSpaces.push(lease)
}
}
Expand All @@ -188,6 +301,9 @@ const parseLeasesForParkingSpaces = (leases: Lease[]): Lease[] | undefined => {

export {
getDetailedApplicantInformation,
addPriorityToApplicantsBasedOnRentalRules,
sortApplicantsBasedOnRentalRules,
assignPriorityToApplicantBasedOnRentalRules,
parseWaitingListForInternalParkingSpace,
parseLeasesForHousingContracts,
parseLeasesForParkingSpaces,
Expand Down
82 changes: 82 additions & 0 deletions src/services/lease-service/tests/factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Factory } from 'fishery'
import { Lease, LeaseStatus, Listing, ListingStatus } from 'onecore-types'
import { leaseTypes } from '../../../constants/leaseTypes'

const LeaseFactory = Factory.define<Lease>(({ sequence }) => ({
leaseId: `${sequence}`,
leaseNumber: `0${sequence}`,
leaseStartDate: new Date(2022, 1),
leaseEndDate: undefined,
status: LeaseStatus.Active,
tenantContactIds: undefined,
tenants: undefined,
rentalPropertyId: '605-703-00-0014',
rentalProperty: undefined,
type: leaseTypes.parkingspaceContract,
rentInfo: undefined,
address: {
street: 'Testgatan',
number: '123',
postalCode: '723 40',
city: 'Västerås',
},
noticeGivenBy: undefined,
noticeDate: undefined,
noticeTimeTenant: undefined,
preferredMoveOutDate: undefined,
terminationDate: undefined,
contractDate: new Date(2021, 11),
lastDebitDate: undefined,
approvalDate: new Date(2021, 12),
residentialArea: {
code: 'MAL',
caption: 'Malmaberg',
},
}))

//todo: use properly defined interface
const ApplicantFactory = Factory.define<any, { currentHousingContract: Lease }>(
({ sequence, params }) => ({
id: `${sequence}`,
name: 'Test Testsson',
contactCode: `P${158769 + sequence}`,
applicationDate: new Date().toISOString(),
applicationType: 'Additional',
status: 1,
listingId: `${sequence}`,
queuePoints: 10,
address: {
street: 'Aromas väg 8B',
number: '',
postalCode: '73439',
city: 'Hallstahammar',
},
currentHousingContract: params.currentHousingContract,
upcomingHousingContract: params.upcomingHousingContract,
parkingSpaceContracts: [],
priority: 0,
})
)

const ListingFactory = Factory.define<Listing>(({ sequence }) => ({
id: sequence + 1,
rentalObjectCode: `R${sequence + 1000}`,
address: 'Sample Address',
monthlyRent: 1000,
districtCaption: 'Malmaberg',
districtCode: 'MAL',
blockCaption: 'LINDAREN 2',
blockCode: '1401',
objectTypeCaption: 'Carport',
objectTypeCode: 'CPORT',
rentalObjectTypeCaption: 'Standard hyresobjektstyp',
rentalObjectTypeCode: 'STD',
publishedFrom: new Date(),
publishedTo: new Date(),
vacantFrom: new Date(),
status: ListingStatus.Active,
waitingListType: 'Bilplats (intern)',
applicants: [],
}))

export { LeaseFactory, ApplicantFactory, ListingFactory }
Loading

0 comments on commit d928f81

Please sign in to comment.