Skip to content

Commit 6bdd570

Browse files
authored
MIM-504: fetch leases by property id from leasing (#291)
* remove unused import * wip lease schemas * lease schema * add route for leases by property id * pass along query params * add content to response * fix query param swagger * route order matters * map from onecore types lease to lease schema type * add basic tests and fix query param schema * rename route
1 parent 7be61fa commit 6bdd570

File tree

4 files changed

+322
-1
lines changed

4 files changed

+322
-1
lines changed

src/adapters/leasing-adapter/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { loggedAxios as axios, logger } from 'onecore-utilities'
22
import { AxiosError } from 'axios'
33
import dayjs from 'dayjs'
4-
import querystring from 'querystring'
54
import {
65
ConsumerReport,
76
Contact,

src/services/lease-service/index.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ import { schemas } from './schemas'
2424
import { isAllowedNumResidents } from './services/is-allowed-num-residents'
2525

2626
import { routes as applicationProfileRoutesOld } from './application-profile-old'
27+
import { registerSchema } from '../../utils/openapi'
28+
import {
29+
GetLeasesByRentalPropertyIdQueryParams,
30+
Lease,
31+
mapLease,
32+
} from './schemas/lease'
2733

2834
const getLeaseWithRelatedEntities = async (rentalId: string) => {
2935
const lease = await leasingAdapter.getLease(rentalId, 'true')
@@ -62,10 +68,102 @@ const getLeasesWithRelatedEntitiesForPnr = async (
6268
* - bearerAuth: []
6369
*/
6470
export const routes = (router: KoaRouter) => {
71+
registerSchema('Lease', Lease)
72+
6573
// TODO: Remove this once all routes are migrated to the new application
6674
// profile (with housing references)
6775
applicationProfileRoutesOld(router)
6876

77+
/**
78+
* @swagger
79+
* /leases/by-rental-property-id/{rentalPropertyId}:
80+
* get:
81+
* summary: Get leases with related entities for a specific rental property id
82+
* tags:
83+
* - Lease service
84+
* description: Retrieves lease information along with related entities (such as tenants, properties, etc.) for the specified rental property id.
85+
* parameters:
86+
* - in: path
87+
* name: rentalPropertyId
88+
* required: true
89+
* schema:
90+
* type: string
91+
* description: Rental roperty id of the building/residence to fetch leases for.
92+
* - in: query
93+
* name: includeUpcomingLeases
94+
* schema:
95+
* type: boolean
96+
* default: false
97+
* description: Whether to include upcoming leases in the response
98+
* - in: query
99+
* name: includeTerminatedLeases
100+
* schema:
101+
* type: boolean
102+
* default: false
103+
* description: Whether to include terminated leases in the response
104+
* - in: query
105+
* name: includeContacts
106+
* schema:
107+
* type: boolean
108+
* default: false
109+
* description: Whether to include contact information in the response
110+
* responses:
111+
* '200':
112+
* description: Successful response with leases and related entities
113+
* content:
114+
* application/json:
115+
* schema:
116+
* type: object
117+
* properties:
118+
* content:
119+
* type: array
120+
* items:
121+
* $ref: '#/components/schemas/Lease'
122+
* '400':
123+
* description: Invalid query parameters
124+
* content:
125+
* application/json:
126+
* schema:
127+
* type: object
128+
* security:
129+
* - bearerAuth: []
130+
*/
131+
router.get(
132+
'(.*)/leases/by-rental-property-id/:rentalPropertyId',
133+
async (ctx) => {
134+
const metadata = generateRouteMetadata(ctx)
135+
const queryParams = GetLeasesByRentalPropertyIdQueryParams.safeParse(
136+
ctx.query
137+
)
138+
139+
if (!queryParams.success) {
140+
ctx.status = 400
141+
ctx.body = {
142+
reason: 'Invalid query parameters',
143+
error: queryParams.error,
144+
...metadata,
145+
}
146+
return
147+
}
148+
149+
try {
150+
const leases = await leasingAdapter.getLeasesForPropertyId(
151+
ctx.params.rentalPropertyId,
152+
queryParams.data
153+
)
154+
155+
ctx.status = 200
156+
ctx.body = {
157+
content: leases.map(mapLease),
158+
...metadata,
159+
}
160+
} catch (err) {
161+
logger.error({ err, metadata }, 'Error fetching leases from leasing')
162+
ctx.status = 500
163+
}
164+
}
165+
)
166+
69167
/**
70168
* @swagger
71169
* /leases/for/{pnr}:
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { Lease as OnecoreTypesLease } from 'onecore-types'
2+
import { z } from 'zod'
3+
4+
/**
5+
* This is a partial zod representation of the current Lease type from onecore-types
6+
* I believe the original type has issues with circular references and decided to leave those out
7+
* as I believe the original Lease type will need some refactoring.
8+
*
9+
* Lease.tenants has a list of contacts, which in turn has a list of leases
10+
* Lease.roomtype has a list of material choices, which also has circular references.
11+
*/
12+
13+
export const Lease = z.object({
14+
leaseId: z.string(),
15+
leaseNumber: z.string(),
16+
leaseStartDate: z.coerce.date(),
17+
leaseEndDate: z.coerce.date().optional(),
18+
status: z.enum(['Current', 'Upcoming', 'AboutToEnd', 'Ended']),
19+
tenantContactIds: z.array(z.string()).optional(),
20+
rentalPropertyId: z.string(),
21+
rentalProperty: z
22+
.object({
23+
rentalPropertyId: z.string(),
24+
apartmentNumber: z.number(),
25+
size: z.number(),
26+
type: z.string(),
27+
address: z
28+
.object({
29+
street: z.string(),
30+
number: z.string(),
31+
postalCode: z.string(),
32+
city: z.string(),
33+
})
34+
.optional(),
35+
rentalPropertyType: z.string(),
36+
additionsIncludedInRent: z.string(),
37+
otherInfo: z.string().optional(),
38+
roomTypes: z
39+
.array(
40+
z.object({
41+
roomTypeId: z.string(),
42+
name: z.string(),
43+
})
44+
)
45+
.optional(),
46+
lastUpdated: z.coerce.date().optional(),
47+
})
48+
.optional(),
49+
type: z.string(),
50+
rentInfo: z
51+
.object({
52+
currentRent: z.object({
53+
rentId: z.string().optional(),
54+
leaseId: z.string().optional(),
55+
currentRent: z.number(),
56+
vat: z.number(),
57+
additionalChargeDescription: z.string().optional(),
58+
additionalChargeAmount: z.number().optional(),
59+
rentStartDate: z.coerce.date().optional(),
60+
rentEndDate: z.coerce.date().optional(),
61+
}),
62+
})
63+
.optional(),
64+
address: z
65+
.object({
66+
street: z.string(),
67+
number: z.string(),
68+
postalCode: z.string(),
69+
city: z.string(),
70+
})
71+
.optional(),
72+
noticeGivenBy: z.string().optional(),
73+
noticeDate: z.coerce.date().optional(),
74+
noticeTimeTenant: z.string().optional(),
75+
preferredMoveOutDate: z.coerce.date().optional(),
76+
terminationDate: z.coerce.date().optional(),
77+
contractDate: z.coerce.date().optional(),
78+
lastDebitDate: z.coerce.date().optional(),
79+
approvalDate: z.coerce.date().optional(),
80+
residentialArea: z
81+
.object({
82+
code: z.string(),
83+
caption: z.string(),
84+
})
85+
.optional(),
86+
tenants: z
87+
.array(
88+
z.object({
89+
contactCode: z.string(),
90+
contactKey: z.string(),
91+
leaseIds: z.array(z.string()).optional(),
92+
firstName: z.string(),
93+
lastName: z.string(),
94+
fullName: z.string(),
95+
nationalRegistrationNumber: z.string(),
96+
birthDate: z.coerce.date(),
97+
address: z
98+
.object({
99+
street: z.string(),
100+
number: z.string(),
101+
postalCode: z.string(),
102+
city: z.string(),
103+
})
104+
.optional(),
105+
phoneNumbers: z
106+
.array(
107+
z.object({
108+
phoneNumber: z.string(),
109+
type: z.string(),
110+
isMainNumber: z.boolean(),
111+
})
112+
)
113+
.optional(),
114+
emailAddress: z.string().optional(),
115+
isTenant: z.boolean(),
116+
parkingSpaceWaitingList: z
117+
.object({
118+
queueTime: z.coerce.date(),
119+
queuePoints: z.number(),
120+
type: z.number(),
121+
})
122+
.optional(),
123+
specialAttention: z.boolean().optional(),
124+
})
125+
)
126+
.optional(),
127+
})
128+
129+
export const GetLeasesByRentalPropertyIdQueryParams = z.object({
130+
includeUpcomingLeases: z
131+
.enum(['true', 'false'])
132+
.optional()
133+
.transform((value) => value === 'true'),
134+
includeTerminatedLeases: z
135+
.enum(['true', 'false'])
136+
.optional()
137+
.transform((value) => value === 'true'),
138+
includeContacts: z
139+
.enum(['true', 'false'])
140+
.optional()
141+
.transform((value) => value === 'true'),
142+
})
143+
144+
export function mapLease(lease: OnecoreTypesLease): z.infer<typeof Lease> {
145+
return {
146+
leaseId: lease.leaseId,
147+
leaseNumber: lease.leaseNumber,
148+
leaseStartDate: lease.leaseStartDate,
149+
leaseEndDate: lease.leaseEndDate,
150+
status:
151+
lease.status === 0
152+
? 'Current'
153+
: lease.status === 1
154+
? 'Upcoming'
155+
: lease.status === 2
156+
? 'AboutToEnd'
157+
: 'Ended',
158+
tenantContactIds: lease.tenantContactIds,
159+
rentalPropertyId: lease.rentalPropertyId,
160+
rentalProperty: lease.rentalProperty,
161+
type: lease.type,
162+
rentInfo: lease.rentInfo,
163+
address: lease.address,
164+
noticeGivenBy: lease.noticeGivenBy,
165+
noticeDate: lease.noticeDate,
166+
noticeTimeTenant: lease.noticeTimeTenant,
167+
preferredMoveOutDate: lease.preferredMoveOutDate,
168+
terminationDate: lease.terminationDate,
169+
contractDate: lease.contractDate,
170+
lastDebitDate: lease.lastDebitDate,
171+
approvalDate: lease.approvalDate,
172+
residentialArea: lease.residentialArea,
173+
tenants: lease.tenants,
174+
}
175+
}

src/services/lease-service/tests/index.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import * as replyToOffer from '../../../processes/parkingspaces/internal/reply-t
1919
import * as factory from '../../../../test/factories'
2020
import { ProcessStatus } from '../../../common/types'
2121
import { schemas } from '../schemas'
22+
import { Lease as LeaseSchema } from '../schemas/lease'
2223

2324
const app = new Koa()
2425
const router = new KoaRouter()
@@ -47,6 +48,54 @@ describe('lease-service', () => {
4748
city: 'Västerås',
4849
}
4950

51+
describe('GET /leases/by-rental-property-id/:rentalPropertyId', () => {
52+
it('responds with 400 for invalid query parameters', async () => {
53+
const res = await request(app.callback()).get(
54+
'/leases/by-rental-property-id/123?includeUpcomingLeases=invalid'
55+
)
56+
57+
expect(res.status).toBe(400)
58+
expect(res.body).toMatchObject({
59+
reason: 'Invalid query parameters',
60+
error: expect.any(Object),
61+
})
62+
})
63+
64+
it('responds with 500 if adapter fails', async () => {
65+
jest
66+
.spyOn(tenantLeaseAdapter, 'getLeasesForPropertyId')
67+
.mockRejectedValue(new Error('Adapter error'))
68+
69+
const res = await request(app.callback()).get(
70+
'/leases/by-rental-property-id/123'
71+
)
72+
73+
expect(res.status).toBe(500)
74+
})
75+
76+
it('responds with a list of leases for valid query parameters', async () => {
77+
const getLeasesForPropertyIdSpy = jest
78+
.spyOn(tenantLeaseAdapter, 'getLeasesForPropertyId')
79+
.mockResolvedValue(factory.lease.buildList(1))
80+
81+
const res = await request(app.callback()).get(
82+
'/leases/by-rental-property-id/123?includeUpcomingLeases=true&includeTerminatedLeases=false&includeContacts=true'
83+
)
84+
85+
expect(res.status).toBe(200)
86+
expect(getLeasesForPropertyIdSpy).toHaveBeenCalledWith(
87+
'123',
88+
expect.objectContaining({
89+
includeUpcomingLeases: true,
90+
includeTerminatedLeases: false,
91+
includeContacts: true,
92+
})
93+
)
94+
95+
expect(() => LeaseSchema.array().parse(res.body.content)).not.toThrow()
96+
})
97+
})
98+
5099
describe('GET /leases/for/:pnr', () => {
51100
it('responds with a list of leases', async () => {
52101
const getLeaseSpy = jest

0 commit comments

Comments
 (0)