diff --git a/src/adapters/property-base-adapter/generated/api-types.ts b/src/adapters/property-base-adapter/generated/api-types.ts index b21bb9be..3223402e 100644 --- a/src/adapters/property-base-adapter/generated/api-types.ts +++ b/src/adapters/property-base-adapter/generated/api-types.ts @@ -446,6 +446,67 @@ export interface paths { patch?: never; trace?: never; }; + "/buildings/by-building-code/{buildingCode}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get detailed information about a specific building by building code + * @description Retrieves comprehensive information about a building using its building code. + * Returns details including construction year, renovation history, insurance information, + * and associated property data. + * + */ + get: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The building code of the building */ + buildingCode: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully retrieved building information */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + content?: components["schemas"]["Building"]; + }; + }; + }; + /** @description Building not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/buildings/{id}": { parameters: { query?: never; @@ -1214,11 +1275,11 @@ export interface components { Building: { id: string; code: string; - name: string; + name: string | null; buildingType: { - id: string; - code: string; - name: string; + id: string | null; + code: string | null; + name: string | null; }; construction: { constructionYear: number | null; diff --git a/src/adapters/property-base-adapter/index.ts b/src/adapters/property-base-adapter/index.ts index eb6a6b8f..46ccc42f 100644 --- a/src/adapters/property-base-adapter/index.ts +++ b/src/adapters/property-base-adapter/index.ts @@ -77,6 +77,34 @@ export async function searchResidences( } } +type GetBuildingResponse = components['schemas']['Building'] + +export async function getBuildingByCode( + buildingCode: string +): Promise> { + try { + const fetchResponse = await client().GET( + '/buildings/by-building-code/{buildingCode}', + { + params: { path: { buildingCode } }, + } + ) + + if (fetchResponse.data?.content) { + return { ok: true, data: fetchResponse.data.content } + } + + if (fetchResponse.response.status === 404) { + return { ok: false, err: 'not-found' } + } + + return { ok: false, err: 'unknown' } + } catch (err) { + logger.error({ err }, 'property-base-adapter.getBuilding') + return { ok: false, err: 'unknown' } + } +} + type GetCompaniesResponse = components['schemas']['Company'][] export async function getCompanies(): Promise< diff --git a/src/adapters/tests/property-base-adapter.test.ts b/src/adapters/tests/property-base-adapter.test.ts index e14d7572..d0480548 100644 --- a/src/adapters/tests/property-base-adapter.test.ts +++ b/src/adapters/tests/property-base-adapter.test.ts @@ -20,6 +20,56 @@ describe('property-base-adapter', () => { mockServer.close() }) + describe('getBuildingByCode', () => { + it('returns err if request fails', async () => { + mockServer.use( + http.get( + `${config.propertyBaseService.url}/buildings/by-building-code/123-123`, + () => new HttpResponse(null, { status: 500 }) + ) + ) + + const result = await propertyBaseAdapter.getBuildingByCode('123-123') + expect(result.ok).toBe(false) + if (!result.ok) expect(result.err).toBe('unknown') + }) + + it('returns not-found if building is not found', async () => { + mockServer.use( + http.get( + `${config.propertyBaseService.url}/buildings/by-building-code/123-123`, + () => new HttpResponse(null, { status: 404 }) + ) + ) + + const result = await propertyBaseAdapter.getBuildingByCode('123-123') + expect(result.ok).toBe(false) + if (!result.ok) expect(result.err).toBe('not-found') + }) + + it('returns building', async () => { + const buildingMock = factory.building.build() + mockServer.use( + http.get( + `${config.propertyBaseService.url}/buildings/by-building-code/123-123`, + () => + HttpResponse.json( + { + content: buildingMock, + }, + { status: 200 } + ) + ) + ) + + const result = await propertyBaseAdapter.getBuildingByCode('123-123') + expect(result).toMatchObject({ + ok: true, + data: buildingMock, + }) + }) + }) + describe('getCompanies', () => { it('returns err if request fails', async () => { mockServer.use( diff --git a/src/services/property-base-service/index.ts b/src/services/property-base-service/index.ts index 4efd7999..a3b60e40 100644 --- a/src/services/property-base-service/index.ts +++ b/src/services/property-base-service/index.ts @@ -7,7 +7,6 @@ import { logger, generateRouteMetadata } from 'onecore-utilities' import { registerSchema } from '../../utils/openapi' import * as schemas from './schemas' import { calculateResidenceStatus } from './calculate-residence-status' -import { z } from 'zod' /** * @swagger @@ -37,6 +36,88 @@ export const routes = (router: KoaRouter) => { schemas.ResidenceByRentalIdSchema ) + /** + * @swagger + * /propertyBase/buildings/by-building-code/{buildingCode}: + * get: + * summary: Get building by building code + * tags: + * - Property base Service + * description: Retrieves building data by building code + * parameters: + * - in: path + * name: buildingCode + * required: true + * schema: + * type: string + * description: The code of the building + * responses: + * '200': + * description: Successfully retrieved building + * content: + * application/json: + * schema: + * type: object + * properties: + * content: + * $ref: '#/components/schemas/Building' + * '404': + * description: Building not found + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: Building not found + * '500': + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: Internal server error + * security: + * - bearerAuth: [] + */ + router.get( + '(.*)/propertyBase/buildings/by-building-code/:buildingCode', + async (ctx) => { + const metadata = generateRouteMetadata(ctx) + const { buildingCode } = ctx.params + + try { + const result = await propertyBaseAdapter.getBuildingByCode(buildingCode) + + if (!result.ok) { + if (result.err === 'not-found') { + ctx.status = 404 + ctx.body = { error: 'Building not found', ...metadata } + return + } + + logger.error(result.err, 'Internal server error', metadata) + ctx.status = 500 + ctx.body = { error: 'Internal server error', ...metadata } + return + } + + ctx.body = { + content: result.data as schemas.Building, + ...metadata, + } + } catch (error) { + logger.error(error, 'Internal server error', metadata) + ctx.status = 500 + ctx.body = { error: 'Internal server error', ...metadata } + } + } + ) + /** * @swagger * /propertyBase/companies: diff --git a/src/services/property-base-service/schemas.ts b/src/services/property-base-service/schemas.ts index 03ff2747..ae8703ae 100644 --- a/src/services/property-base-service/schemas.ts +++ b/src/services/property-base-service/schemas.ts @@ -1,5 +1,30 @@ import { z } from 'zod' +export const BuildingSchema = z.object({ + id: z.string(), + code: z.string(), + name: z.string(), + buildingType: z.object({ + id: z.string(), + code: z.string(), + name: z.string(), + }), + construction: z.object({ + constructionYear: z.number(), + renovationYear: z.number(), + valueYear: z.number().nullable(), + }), + features: z.object({ + heating: z.string().nullable(), + fireRating: z.string().nullable(), + }), + insurance: z.object({ + class: z.string().nullable(), + value: z.number().nullable(), + }), + deleted: z.boolean(), +}) + export const CompanySchema = z.object({ id: z.string(), propertyObjectId: z.string(), @@ -322,6 +347,7 @@ export const StaircasesQueryParamsSchema = z.object({ .min(7, { message: 'buildingCode must be at least 7 characters long.' }), }) +export type Building = z.infer export type Company = z.infer export type Property = z.infer export type PropertyDetails = z.infer diff --git a/src/services/property-base-service/tests/index.test.ts b/src/services/property-base-service/tests/index.test.ts index 547626ea..7588a54f 100644 --- a/src/services/property-base-service/tests/index.test.ts +++ b/src/services/property-base-service/tests/index.test.ts @@ -28,6 +28,51 @@ app.use(router.routes()) beforeEach(jest.resetAllMocks) describe('property-base-service', () => { + describe('GET /propertyBase/buildings/by-building-code/:buildingCode', () => { + it('returns 200 and a building by code', async () => { + const buildingMock = factory.building.build() + const getBuildingSpy = jest + .spyOn(propertyBaseAdapter, 'getBuildingByCode') + .mockResolvedValueOnce({ ok: true, data: buildingMock }) + + const res = await request(app.callback()).get( + `/propertyBase/buildings/by-building-code/${buildingMock.code}` + ) + + expect(res.status).toBe(200) + expect(getBuildingSpy).toHaveBeenCalledWith(buildingMock.code) + expect(JSON.stringify(res.body.content)).toEqual( + JSON.stringify(buildingMock) + ) + }) + + it('returns 404 if no building is found', async () => { + const getBuildingSpy = jest + .spyOn(propertyBaseAdapter, 'getBuildingByCode') + .mockResolvedValueOnce({ ok: false, err: 'not-found' }) + + const res = await request(app.callback()).get( + '/propertyBase/buildings/by-building-code/123-456' + ) + + expect(res.status).toBe(404) + expect(getBuildingSpy).toHaveBeenCalledWith('123-456') + }) + + it('returns 500 if an error occurs', async () => { + const getBuildingSpy = jest + .spyOn(propertyBaseAdapter, 'getBuildingByCode') + .mockResolvedValueOnce({ ok: false, err: 'unknown' }) + + const res = await request(app.callback()).get( + '/propertyBase/buildings/by-building-code/123-456' + ) + + expect(res.status).toBe(500) + expect(getBuildingSpy).toHaveBeenCalledWith('123-456') + }) + }) + describe('GET /propertyBase/companies', () => { it('returns 200 and a list of companies', async () => { const companiesMock = factory.company.buildList(3) diff --git a/src/services/search-service/schemas.ts b/src/services/search-service/schemas.ts index 07a3736b..c6efa033 100644 --- a/src/services/search-service/schemas.ts +++ b/src/services/search-service/schemas.ts @@ -9,7 +9,7 @@ export const PropertySearchResultSchema = z.object({ export const BuildingSearchResultSchema = z.object({ id: z.string().describe('Unique identifier for the search result'), type: z.literal('building').describe('Indicates this is a building result'), - name: z.string().describe('Name of the building'), + name: z.string().nullable().describe('Name of the building'), property: z .object({ name: z diff --git a/test/factories/building.ts b/test/factories/building.ts new file mode 100644 index 00000000..214ef0d4 --- /dev/null +++ b/test/factories/building.ts @@ -0,0 +1,30 @@ +import { Factory } from 'fishery' +import { components } from '../../src/adapters/property-base-adapter/generated/api-types' + +export const BuildingFactory = Factory.define< + components['schemas']['Building'] +>(({ sequence }) => ({ + id: `building-${sequence}`, + code: `123-12${sequence}`, + name: 'Test Building', + buildingType: { + id: 'type-1', + code: `T00${sequence}`, + name: 'Test Building', + }, + construction: { + constructionYear: 2020, + renovationYear: 1999, + valueYear: null, + }, + features: { + heating: null, + fireRating: null, + }, + insurance: { + class: null, + value: null, + }, + deleted: false, + property: null, +})) diff --git a/test/factories/index.ts b/test/factories/index.ts index 5b0e30de..3f43a2e9 100644 --- a/test/factories/index.ts +++ b/test/factories/index.ts @@ -1,3 +1,4 @@ +export { BuildingFactory as building } from './building' export { DetailedApplicantFactory as detailedApplicant } from './detailed-applicant' export { ListingFactory as listing } from './listing' export { ParkingSpaceFactory as parkingSpace } from './parking-space'