From 3c9fc90eac4b5a19980f89e97bac3219de47a45d Mon Sep 17 00:00:00 2001 From: Andrea Gueugnaut Date: Wed, 27 Nov 2024 15:02:43 +0100 Subject: [PATCH 01/11] refactor: change the housing creation endpoint to POST /housing --- frontend/src/services/housing.service.ts | 2 +- server/src/routers/protected.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/services/housing.service.ts b/frontend/src/services/housing.service.ts index 36dba239c..84de44c29 100644 --- a/frontend/src/services/housing.service.ts +++ b/frontend/src/services/housing.service.ts @@ -93,7 +93,7 @@ export const housingApi = zlvApi.injectEndpoints({ // TODO: fix this any type createHousing: builder.mutation({ query: (payload) => ({ - url: 'housing/creation', + url: 'housing', method: 'POST', body: payload }), diff --git a/server/src/routers/protected.ts b/server/src/routers/protected.ts index 8b79dd636..0a416fb83 100644 --- a/server/src/routers/protected.ts +++ b/server/src/routers/protected.ts @@ -47,7 +47,7 @@ router.get( ); // TODO: replace by POST /housing router.post( - '/housing/creation', + '/housing', housingController.createValidators, validator.validate, housingController.create From 8c33e73726cc983bd050ee5760c22e0affb3f187 Mon Sep 17 00:00:00 2001 From: Andrea Gueugnaut Date: Wed, 27 Nov 2024 15:03:39 +0100 Subject: [PATCH 02/11] refactor(server): test and rework onConflict --- server/src/infra/database/index.ts | 20 ++-- server/src/infra/database/test/index.test.ts | 106 +++++++++++++++++++ 2 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 server/src/infra/database/test/index.test.ts diff --git a/server/src/infra/database/index.ts b/server/src/infra/database/index.ts index cc6317b9d..2c666b42e 100644 --- a/server/src/infra/database/index.ts +++ b/server/src/infra/database/index.ts @@ -1,5 +1,6 @@ import { Knex, knex } from 'knex'; import fp from 'lodash/fp'; +import { match } from 'ts-pattern'; import config from '~/infra/database/knexfile'; import { compact } from '~/utils/object'; @@ -50,19 +51,22 @@ export function groupBy(props?: Array) { } export interface ConflictOptions { - onConflict?: ReadonlyArray; - merge?: ReadonlyArray; + onConflict: ReadonlyArray; + merge: boolean | ReadonlyArray; } -export function onConflict(opts?: ConflictOptions) { +export function onConflict(opts: ConflictOptions) { return (query: Knex.QueryBuilder): void => { - if (opts?.onConflict && opts.onConflict.length === 0) { - query.onConflict(opts.onConflict as any).ignore(); + if (opts.onConflict.length === 0) { + throw new Error('onConflict must have at least one column'); } - if (opts?.onConflict && opts.onConflict.length > 0) { - query.onConflict(opts.onConflict as any).merge(opts?.merge); - } + const onConflict = opts.onConflict as ReadonlyArray; + match(opts.merge) + .returnType() + .with(false, () => query.onConflict(onConflict).ignore()) + .with(true, () => query.onConflict(onConflict).merge()) + .otherwise((columns) => query.onConflict(onConflict).merge(columns)); }; } diff --git a/server/src/infra/database/test/index.test.ts b/server/src/infra/database/test/index.test.ts new file mode 100644 index 000000000..8f3bada83 --- /dev/null +++ b/server/src/infra/database/test/index.test.ts @@ -0,0 +1,106 @@ +import db, { onConflict } from '~/infra/database'; +import { faker } from '@faker-js/faker/locale/fr'; + +describe('Database utils', () => { + const TEST_TABLE = 'test'; + + interface TestEntity { + id: string; + a: string; + b: number; + } + + function Tests() { + return db(TEST_TABLE); + } + + beforeAll(async () => { + const tableExists = await db.schema.hasTable(TEST_TABLE); + if (!tableExists) { + await db.schema.createTable(TEST_TABLE, (table) => { + table.uuid('id').primary().notNullable(); + table.string('a').notNullable(); + table.integer('b').notNullable(); + }); + } + }); + + afterAll(async () => { + await db.schema.dropTableIfExists(TEST_TABLE); + }); + + describe('onConflict', () => { + let entity: TestEntity; + + beforeEach(async () => { + entity = { + id: faker.string.uuid(), + a: faker.string.sample(), + b: faker.number.int({ max: 1000 }) + }; + await Tests().insert(entity); + }); + + it('should ignore conflicts if the merge option is false', async () => { + await Tests() + .insert({ + id: entity.id, + a: faker.string.sample(), + b: faker.number.int({ max: 1000 }) + }) + .modify( + onConflict({ + onConflict: ['id'], + merge: false + }) + ); + + const actual = await Tests().where({ id: entity.id }).first(); + expect(actual).toStrictEqual(entity); + }); + + it('should fully update the entity if the merge option is true', async () => { + const replacement: TestEntity = { + id: entity.id, + a: faker.string.sample(), + b: faker.number.int({ max: 1000 }) + }; + + await Tests() + .insert(replacement) + .modify( + onConflict({ + onConflict: ['id'], + merge: true + }) + ); + + const actual = await Tests().where({ id: entity.id }).first(); + expect(actual).toStrictEqual(replacement); + }); + + it('should update the given properties if the merge option contains keys', async () => { + const replacement: TestEntity = { + id: entity.id, + a: faker.string.sample(), + b: faker.number.int({ max: 1000 }) + }; + + await Tests() + .insert(replacement) + .modify( + onConflict({ + onConflict: ['id'], + merge: ['b'] + }) + ); + + const actual = await Tests().where({ id: entity.id }).first(); + expect(actual).toStrictEqual({ + id: entity.id, + a: entity.a, + b: replacement.b + }); + }); + }); +}); From c02b6c085f99e8f7894a74754881e29ab2d1feaf Mon Sep 17 00:00:00 2001 From: Andrea Gueugnaut Date: Wed, 27 Nov 2024 15:09:52 +0100 Subject: [PATCH 03/11] test(server): test owners update on housing creation --- .../src/controllers/housingController.test.ts | 67 +++++++++++++++++-- server/src/controllers/housingController.ts | 5 +- server/src/controllers/ownerController.ts | 5 +- server/src/repositories/ownerRepository.ts | 2 +- 4 files changed, 70 insertions(+), 9 deletions(-) diff --git a/server/src/controllers/housingController.test.ts b/server/src/controllers/housingController.test.ts index 5e2131534..8ca830bd3 100644 --- a/server/src/controllers/housingController.test.ts +++ b/server/src/controllers/housingController.test.ts @@ -6,7 +6,8 @@ import db from '~/infra/database'; import { tokenProvider } from '~/test/testUtils'; import { formatHousingRecordApi, - Housing + Housing, + housingTable } from '~/repositories/housingRepository'; import { genCampaignApi, @@ -18,7 +19,12 @@ import { genUserApi, oneOf } from '~/test/testFixtures'; -import { formatOwnerApi, Owners } from '~/repositories/ownerRepository'; +import { + formatOwnerApi, + OwnerRecordDBO, + Owners, + ownerTable +} from '~/repositories/ownerRepository'; import { HousingStatusApi } from '~/models/HousingStatusApi'; import { Events, @@ -32,7 +38,8 @@ import { HousingUpdateBody } from './housingController'; import { housingNotesTable, Notes } from '~/repositories/noteRepository'; import { formatHousingOwnersApi, - HousingOwners + HousingOwners, + housingOwnersTable } from '~/repositories/housingOwnerRepository'; import { DatafoncierHouses } from '~/repositories/datafoncierHousingRepository'; import { DatafoncierOwners } from '~/repositories/datafoncierOwnersRepository'; @@ -50,6 +57,7 @@ import { formatCampaignHousingApi } from '~/repositories/campaignHousingRepository'; import { faker } from '@faker-js/faker/locale/fr'; +import { OwnerApi } from '~/models/OwnerApi'; describe('Housing API', () => { const { app } = createServer(); @@ -175,8 +183,8 @@ describe('Housing API', () => { }); }); - describe('POST /housing/creation', () => { - const testRoute = '/api/housing/creation'; + describe('POST /housing', () => { + const testRoute = '/api/housing'; it('should be forbidden a non-authenticated user', async () => { const { status } = await request(app).post(testRoute); @@ -234,6 +242,55 @@ describe('Housing API', () => { }); }); + it('should ignore owners update if they already exist', async () => { + const datafoncierHousing = genDatafoncierHousing(); + const datafoncierOwners = Array.from({ length: 3 }, () => + genDatafoncierOwner(datafoncierHousing.idprocpte) + ); + const existingOwners = datafoncierOwners.map( + (datafoncierOwner) => { + return { + ...genOwnerApi(), + idpersonne: datafoncierOwner.idpersonne + }; + } + ); + await Promise.all([ + DatafoncierHouses().insert(datafoncierHousing), + DatafoncierOwners().insert(datafoncierOwners), + Owners().insert(existingOwners.map(formatOwnerApi)) + ]); + const payload = { + localId: datafoncierHousing.idlocal + }; + + const { status } = await request(app) + .post(testRoute) + .send(payload) + .type('json') + .use(tokenProvider(user)); + + expect(status).toBe(constants.HTTP_STATUS_CREATED); + const actualOwners = await Owners() + .select(`${ownerTable}.*`) + .join( + housingOwnersTable, + `${housingOwnersTable}.owner_id`, + `${ownerTable}.id` + ) + .join( + housingTable, + `${housingTable}.id`, + `${housingOwnersTable}.housing_id` + ) + .where(`${housingTable}.local_id`, datafoncierHousing.idlocal); + expect(actualOwners).toSatisfyAll((actualOwner) => { + return existingOwners.some((existingOwner) => { + return existingOwner.id === actualOwner.id; + }); + }); + }); + it('should assign its owners', async () => { const datafoncierHousing = genDatafoncierHousing(); const datafoncierOwners = Array.from({ length: 6 }, () => diff --git a/server/src/controllers/housingController.ts b/server/src/controllers/housingController.ts index 020fe6fd5..2d5b5eb78 100644 --- a/server/src/controllers/housingController.ts +++ b/server/src/controllers/housingController.ts @@ -58,7 +58,7 @@ async function get(request: Request, response: Response) { id, localId, includes: ['events', 'owner', 'perimeters', 'campaigns'] - }); + }); if (!housing) { throw new HousingMissingError(params.id); } @@ -205,7 +205,8 @@ async function create(request: Request, response: Response) { await async.forEach(datafoncierOwners, async (datafoncierOwner) => { const owner = toOwnerApi(datafoncierOwner); await ownerRepository.betterSave(owner, { - onConflict: ['idpersonne'] + onConflict: ['idpersonne'], + merge: false }); }); const owners = await ownerRepository.find({ diff --git a/server/src/controllers/ownerController.ts b/server/src/controllers/ownerController.ts index d0c52f070..e7b5c2682 100644 --- a/server/src/controllers/ownerController.ts +++ b/server/src/controllers/ownerController.ts @@ -100,7 +100,10 @@ async function create( updatedAt: new Date().toJSON() }; - await ownerRepository.betterSave(owner); + await ownerRepository.betterSave(owner, { + onConflict: ['id'], + merge: false + }); await banAddressesRepository.markAddressToBeNormalized( owner.id, AddressKinds.Owner diff --git a/server/src/repositories/ownerRepository.ts b/server/src/repositories/ownerRepository.ts index 07b07416e..66dc09a82 100644 --- a/server/src/repositories/ownerRepository.ts +++ b/server/src/repositories/ownerRepository.ts @@ -291,7 +291,7 @@ type BetterSaveOptions = ConflictOptions; */ async function betterSave( owner: OwnerApi, - opts?: BetterSaveOptions + opts: BetterSaveOptions ): Promise { logger.debug(`Saving owner...`, { owner }); await Owners().insert(formatOwnerApi(owner)).modify(onConflict(opts)); From 25ef101486e52eb55fca7ccb544736b67000cb6a Mon Sep 17 00:00:00 2001 From: Andrea Gueugnaut Date: Wed, 27 Nov 2024 16:11:52 +0100 Subject: [PATCH 04/11] =?UTF-8?q?refactor:=20change=20occupancy=E2=80=99s?= =?UTF-8?q?=20type=20to=20the=20shared=20occupancy=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/models/src/DatafoncierHousing.ts | 9 +++- .../src/test/DatafoncierHousing.test.ts | 24 ++++++++++ .../src/controllers/housingController.test.ts | 15 +++--- server/src/controllers/housingController.ts | 2 +- server/src/models/HousingApi.ts | 8 ++-- server/src/models/HousingFiltersApi.ts | 6 +-- server/src/repositories/housingRepository.ts | 26 ++++------ .../test/housingRepository.test.ts | 48 +++++++++---------- .../housings/housing-processor.ts | 13 ++--- .../housings/test/housing-processor.test.ts | 14 +++--- .../source-housing-processor.ts | 9 ++-- .../test/source-housing-command.test.ts | 11 +++-- .../shared/models/DatafoncierHousing.ts | 16 +++---- server/src/test/testFixtures.ts | 18 +++---- 14 files changed, 118 insertions(+), 101 deletions(-) create mode 100644 packages/models/src/test/DatafoncierHousing.test.ts diff --git a/packages/models/src/DatafoncierHousing.ts b/packages/models/src/DatafoncierHousing.ts index ad26feaaa..b54a7ebeb 100644 --- a/packages/models/src/DatafoncierHousing.ts +++ b/packages/models/src/DatafoncierHousing.ts @@ -1,5 +1,7 @@ import fp from 'lodash/fp'; +import { Occupancy, OCCUPANCY_VALUES } from './Occupancy'; + /** * @see http://doc-datafoncier.cerema.fr/ff/doc_fftp/table/pb0010_local/last/ */ @@ -57,7 +59,7 @@ export interface DatafoncierHousing { dloy48a: number | null; top48a: string; dnatlc: string; - ccthp: string; + ccthp: string | null; proba_rprs: string; typeact: string | null; loghvac: string | null; @@ -140,3 +142,8 @@ export function toAddress(housing: DatafoncierHousing): string { const city = housing.idcomtxt; return `${streetNumber}${repetition} ${street}, ${zipcode} ${city}`; } + +export function toOccupancy(ccthp: DatafoncierHousing['ccthp']): Occupancy { + const occupancy = OCCUPANCY_VALUES.find((occupancy) => occupancy === ccthp); + return occupancy ?? Occupancy.UNKNOWN; +} diff --git a/packages/models/src/test/DatafoncierHousing.test.ts b/packages/models/src/test/DatafoncierHousing.test.ts new file mode 100644 index 000000000..3ff435570 --- /dev/null +++ b/packages/models/src/test/DatafoncierHousing.test.ts @@ -0,0 +1,24 @@ +import { Occupancy, OCCUPANCY_VALUES } from '../Occupancy'; +import { toOccupancy } from '../DatafoncierHousing'; + +describe('DatafoncierHousing', () => { + describe('toOccupancy', () => { + it.each(OCCUPANCY_VALUES)('should return %s', (ccthp) => { + const actual = toOccupancy(ccthp); + + expect(actual).toBe(ccthp); + }); + + it(`should return ${Occupancy.UNKNOWN} when ccthp is null`, () => { + const actual = toOccupancy(null); + + expect(actual).toBe(Occupancy.UNKNOWN); + }); + + it(`should return ${Occupancy.UNKNOWN} otherwise`, () => { + const actual = toOccupancy('test'); + + expect(actual).toBe(Occupancy.UNKNOWN); + }); + }); +}); diff --git a/server/src/controllers/housingController.test.ts b/server/src/controllers/housingController.test.ts index 8ca830bd3..809b93b1c 100644 --- a/server/src/controllers/housingController.test.ts +++ b/server/src/controllers/housingController.test.ts @@ -58,6 +58,7 @@ import { } from '~/repositories/campaignHousingRepository'; import { faker } from '@faker-js/faker/locale/fr'; import { OwnerApi } from '~/models/OwnerApi'; +import { Occupancy, OCCUPANCY_VALUES } from '@zerologementvacant/models'; describe('Housing API', () => { const { app } = createServer(); @@ -151,12 +152,10 @@ describe('Housing API', () => { }); it('should sort housings by occupancy', async () => { - const housings = Object.values(OccupancyKindApi).map( - (occupancy) => ({ - ...genHousingApi(faker.helpers.arrayElement(establishment.geoCodes)), - occupancy - }) - ); + const housings = OCCUPANCY_VALUES.map((occupancy) => ({ + ...genHousingApi(faker.helpers.arrayElement(establishment.geoCodes)), + occupancy + })); const owner = genOwnerApi(); await Promise.all([ Housing().insert(housings.map(formatHousingRecordApi)), @@ -357,8 +356,8 @@ describe('Housing API', () => { vacancyReasons: [randomstring.generate()] }, occupancyUpdate: { - occupancy: OccupancyKindApi.Vacant, - occupancyIntended: OccupancyKindApi.DemolishedOrDivided + occupancy: Occupancy.VACANT, + occupancyIntended: Occupancy.DEMOLISHED_OR_DIVIDED }, note: { content: randomstring.generate(), diff --git a/server/src/controllers/housingController.ts b/server/src/controllers/housingController.ts index 2d5b5eb78..b866eadb1 100644 --- a/server/src/controllers/housingController.ts +++ b/server/src/controllers/housingController.ts @@ -110,7 +110,7 @@ async function list( ), energyConsumption: rawFilters?.energyConsumption as unknown as EnergyConsumptionGradesApi[], - occupancies: rawFilters?.occupancies as unknown as OccupancyKindApi[], + occupancies: rawFilters?.occupancies, establishmentIds: [UserRoles.Admin, UserRoles.Visitor].includes(role) && rawFilters?.establishmentIds?.length diff --git a/server/src/models/HousingApi.ts b/server/src/models/HousingApi.ts index 61815e6f4..0785bd5df 100644 --- a/server/src/models/HousingApi.ts +++ b/server/src/models/HousingApi.ts @@ -1,7 +1,7 @@ import fp from 'lodash/fp'; import { assert, MarkRequired } from 'ts-essentials'; -import { HousingSource } from '@zerologementvacant/models'; +import { HousingSource, Occupancy } from '@zerologementvacant/models'; import { OwnerApi } from './OwnerApi'; import { HousingStatusApi } from './HousingStatusApi'; import { Sort } from './SortApi'; @@ -52,9 +52,9 @@ export interface HousingRecordApi { precisions?: string[]; energyConsumption?: EnergyConsumptionGradesApi; energyConsumptionAt?: Date; - occupancy: OccupancyKindApi; - occupancyRegistered: OccupancyKindApi; - occupancyIntended?: OccupancyKindApi; + occupancy: Occupancy; + occupancyRegistered: Occupancy; + occupancyIntended?: Occupancy; source: HousingSource | null; } diff --git a/server/src/models/HousingFiltersApi.ts b/server/src/models/HousingFiltersApi.ts index dafeb1e05..cc09efa43 100644 --- a/server/src/models/HousingFiltersApi.ts +++ b/server/src/models/HousingFiltersApi.ts @@ -1,5 +1,5 @@ -import { OwnershipKind } from '@zerologementvacant/models'; -import { EnergyConsumptionGradesApi, OccupancyKindApi } from './HousingApi'; +import { Occupancy, OwnershipKind } from '@zerologementvacant/models'; +import { EnergyConsumptionGradesApi } from './HousingApi'; import { body, ValidationChain } from 'express-validator'; import { isArrayOf, isInteger, isString, isUUID } from '~/utils/validators'; @@ -40,7 +40,7 @@ export interface HousingFiltersApi { subStatus?: string[]; query?: string; energyConsumption?: EnergyConsumptionGradesApi[]; - occupancies?: OccupancyKindApi[]; + occupancies?: Occupancy[]; } const validators = (property = 'filters'): ValidationChain[] => [ diff --git a/server/src/repositories/housingRepository.ts b/server/src/repositories/housingRepository.ts index e9da75fef..a70eb3e2f 100644 --- a/server/src/repositories/housingRepository.ts +++ b/server/src/repositories/housingRepository.ts @@ -8,6 +8,7 @@ import { HousingSource, INTERNAL_CO_CONDOMINIUM_VALUES, INTERNAL_MONO_CONDOMINIUM_VALUES, + Occupancy, PaginationOptions } from '@zerologementvacant/models'; import db, { toRawArray, where } from '~/infra/database'; @@ -15,8 +16,7 @@ import { EnergyConsumptionGradesApi, HousingApi, HousingRecordApi, - HousingSortApi, - OccupancyKindApi + HousingSortApi } from '~/models/HousingApi'; import { OwnerDBO, ownerTable, parseOwnerApi } from './ownerRepository'; import { HousingFiltersApi } from '~/models/HousingFiltersApi'; @@ -608,23 +608,13 @@ function filteredQuery(opts: ListQueryOptions) { whereBuilder.orWhere('vacancy_start_year', 2019); } if (filters.vacancyYears?.includes('2018to2015')) { - whereBuilder.orWhereBetween('vacancy_start_year', [ - 2015, - 2018 - ]); + whereBuilder.orWhereBetween('vacancy_start_year', [2015, 2018]); } if (filters.vacancyYears?.includes('2014to2010')) { - whereBuilder.orWhereBetween('vacancy_start_year', [ - 2010, - 2014 - ]); + whereBuilder.orWhereBetween('vacancy_start_year', [2010, 2014]); } if (filters.vacancyYears?.includes('before2010')) { - whereBuilder.orWhere( - 'vacancy_start_year', - '<', - 2010 - ); + whereBuilder.orWhere('vacancy_start_year', '<', 2010); } if (filters.vacancyYears?.includes('missingData')) { whereBuilder.orWhere('vacancy_start_year', 0); @@ -907,9 +897,9 @@ export interface HousingRecordDBO { status: HousingStatusApi; sub_status?: string | null; precisions?: string[]; - occupancy: OccupancyKindApi; - occupancy_source: OccupancyKindApi; - occupancy_intended?: OccupancyKindApi; + occupancy: Occupancy; + occupancy_source: Occupancy; + occupancy_intended?: Occupancy; energy_consumption_bdnb?: EnergyConsumptionGradesApi; energy_consumption_at_bdnb?: Date; } diff --git a/server/src/repositories/test/housingRepository.test.ts b/server/src/repositories/test/housingRepository.test.ts index 1a458bab7..d5fcf8a43 100644 --- a/server/src/repositories/test/housingRepository.test.ts +++ b/server/src/repositories/test/housingRepository.test.ts @@ -36,11 +36,7 @@ import { HousingOwnerDBO, HousingOwners } from '../housingOwnerRepository'; -import { - EnergyConsumptionGradesApi, - HousingApi, - OccupancyKindApi -} from '~/models/HousingApi'; +import { EnergyConsumptionGradesApi, HousingApi } from '~/models/HousingApi'; import { formatLocalityApi, Localities } from '../localityRepository'; import { LocalityApi } from '~/models/LocalityApi'; import { BuildingApi } from '~/models/BuildingApi'; @@ -69,6 +65,8 @@ import { INTERNAL_CO_CONDOMINIUM_VALUES, INTERNAL_MONO_CONDOMINIUM_VALUES, isSecondaryOwner, + Occupancy, + OCCUPANCY_VALUES, OwnershipKind, ROOM_COUNT_VALUES } from '@zerologementvacant/models'; @@ -181,12 +179,10 @@ describe('Housing repository', () => { describe('by occupancy', () => { beforeEach(async () => { - const housings: HousingApi[] = Object.values(OccupancyKindApi).map( - (occupancy) => ({ - ...genHousingApi(), - occupancy - }) - ); + const housings: HousingApi[] = OCCUPANCY_VALUES.map((occupancy) => ({ + ...genHousingApi(), + occupancy + })); await Housing().insert(housings.map(formatHousingRecordApi)); const owner = genOwnerApi(); await Owners().insert(formatOwnerApi(owner)); @@ -197,7 +193,7 @@ describe('Housing repository', () => { ); }); - test.each(Object.values(OccupancyKindApi))( + test.each(OCCUPANCY_VALUES)( 'should filter by %s', async (occupancy) => { const actual = await housingRepository.find({ @@ -871,10 +867,13 @@ describe('Housing repository', () => { .map((_, i) => ({ ...genHousingApi(), vacancyStartYear: ReferenceDataYear - i - })).concat([ { - ...genHousingApi(), - vacancyStartYear: 0 - } ]); + })) + .concat([ + { + ...genHousingApi(), + vacancyStartYear: 0 + } + ]); await Housing().insert(housingList.map(formatHousingRecordApi)); }); @@ -902,7 +901,7 @@ describe('Housing repository', () => { filter: ['2018to2015'], predicate: (housing: HousingApi) => { const vacancyStartYear = housing.vacancyStartYear as number; - return vacancyStartYear >= 2015 && vacancyStartYear <= 2018; + return vacancyStartYear >= 2015 && vacancyStartYear <= 2018; } }, { @@ -1177,8 +1176,7 @@ describe('Housing repository', () => { return new Array(vacant + other).fill('0').map((_, i) => ({ ...genHousingApi(), buildingId, - occupancy: - i < vacant ? OccupancyKindApi.Vacant : OccupancyKindApi.Rent + occupancy: i < vacant ? Occupancy.VACANT : Occupancy.RENT })); } @@ -1651,8 +1649,8 @@ describe('Housing repository', () => { await Housing().insert(formatHousingRecordApi(original)); const update: HousingApi = { ...original, - occupancy: OccupancyKindApi.Rent, - occupancyIntended: OccupancyKindApi.CommercialOrOffice + occupancy: Occupancy.RENT, + occupancyIntended: Occupancy.COMMERCIAL_OR_OFFICE }; await housingRepository.save(update, { onConflict: 'merge' }); @@ -1668,14 +1666,14 @@ describe('Housing repository', () => { it('should update specific fields of an existing housing', async () => { const original: HousingApi = { ...genHousingApi(oneOf(establishment.geoCodes)), - occupancy: OccupancyKindApi.Vacant, - occupancyIntended: OccupancyKindApi.Rent + occupancy: Occupancy.VACANT, + occupancyIntended: Occupancy.RENT }; await Housing().insert(formatHousingRecordApi(original)); const update: HousingApi = { ...original, - occupancy: OccupancyKindApi.Rent, - occupancyIntended: OccupancyKindApi.CommercialOrOffice + occupancy: Occupancy.RENT, + occupancyIntended: Occupancy.COMMERCIAL_OR_OFFICE }; await housingRepository.save(update, { diff --git a/server/src/scripts/import-lovac/housings/housing-processor.ts b/server/src/scripts/import-lovac/housings/housing-processor.ts index fe00d0219..4eaa28e18 100644 --- a/server/src/scripts/import-lovac/housings/housing-processor.ts +++ b/server/src/scripts/import-lovac/housings/housing-processor.ts @@ -1,9 +1,10 @@ import { WritableStream } from 'node:stream/web'; import { v4 as uuidv4 } from 'uuid'; +import { Occupancy } from '@zerologementvacant/models'; import { ReporterError, ReporterOptions } from '~/scripts/import-lovac/infra'; import { createLogger } from '~/infra/logger'; -import { HousingApi, OccupancyKindApi } from '~/models/HousingApi'; +import { HousingApi } from '~/models/HousingApi'; import { HousingStatusApi } from '~/models/HousingStatusApi'; import { HousingEventApi } from '~/models/EventApi'; import { UserApi } from '~/models/UserApi'; @@ -38,13 +39,13 @@ export function createHousingProcessor(opts: ProcessorOptions) { logger.debug('Processing housing...', { chunk }); if (!chunk.dataFileYears.includes('lovac-2024')) { - if (chunk.occupancy === OccupancyKindApi.Vacant) { + if (chunk.occupancy === Occupancy.VACANT) { if (!isInProgress(chunk) && !isCompleted(chunk)) { await Promise.all([ housingRepository.update( { geoCode: chunk.geoCode, id: chunk.id }, { - occupancy: OccupancyKindApi.Unknown, + occupancy: Occupancy.UNKNOWN, status: HousingStatusApi.Completed, subStatus: 'Sortie de la vacance' } @@ -57,7 +58,7 @@ export function createHousingProcessor(opts: ProcessorOptions) { section: 'Situation', conflict: false, old: chunk, - new: { ...chunk, occupancy: OccupancyKindApi.Unknown }, + new: { ...chunk, occupancy: Occupancy.UNKNOWN }, createdAt: new Date(), createdBy: auth.id, housingId: chunk.id, @@ -71,10 +72,10 @@ export function createHousingProcessor(opts: ProcessorOptions) { section: 'Situation', conflict: false, // This event should come after the above one - old: { ...chunk, occupancy: OccupancyKindApi.Unknown }, + old: { ...chunk, occupancy: Occupancy.UNKNOWN }, new: { ...chunk, - occupancy: OccupancyKindApi.Unknown, + occupancy: Occupancy.UNKNOWN, status: HousingStatusApi.Completed, subStatus: 'Sortie de la vacance' }, diff --git a/server/src/scripts/import-lovac/housings/test/housing-processor.test.ts b/server/src/scripts/import-lovac/housings/test/housing-processor.test.ts index c514b1da6..6c585341c 100644 --- a/server/src/scripts/import-lovac/housings/test/housing-processor.test.ts +++ b/server/src/scripts/import-lovac/housings/test/housing-processor.test.ts @@ -1,4 +1,6 @@ import { ReadableStream } from 'node:stream/web'; + +import { Occupancy } from '@zerologementvacant/models'; import { genHousingApi, genUserApi } from '~/test/testFixtures'; import { createHousingProcessor, @@ -7,7 +9,7 @@ import { ProcessorOptions } from '~/scripts/import-lovac/housings/housing-processor'; import { createNoopReporter } from '~/scripts/import-lovac/infra/reporters/noop-reporter'; -import { HousingApi, OccupancyKindApi } from '~/models/HousingApi'; +import { HousingApi } from '~/models/HousingApi'; import { HOUSING_STATUS_VALUES, HousingStatusApi @@ -67,7 +69,7 @@ describe('Housing processor', () => { describe('if it is vacant', () => { beforeEach(() => { - housing.occupancy = OccupancyKindApi.Vacant; + housing.occupancy = Occupancy.VACANT; }); describe('if it is currently monitored', () => { @@ -154,7 +156,7 @@ describe('Housing processor', () => { expect(housingRepository.update).toHaveBeenCalledWith( { id: housing.id, geoCode: housing.geoCode }, expect.objectContaining({ - occupancy: OccupancyKindApi.Unknown, + occupancy: Occupancy.UNKNOWN, status: HousingStatusApi.Completed, subStatus: 'Sortie de la vacance' }) @@ -190,7 +192,7 @@ describe('Housing processor', () => { section: 'Situation', conflict: false, old: housing, - new: { ...housing, occupancy: OccupancyKindApi.Unknown }, + new: { ...housing, occupancy: Occupancy.UNKNOWN }, createdAt: expect.any(Date), createdBy: expect.any(String), housingId: housing.id, @@ -226,10 +228,10 @@ describe('Housing processor', () => { category: 'Followup', section: 'Situation', conflict: false, - old: { ...housing, occupancy: OccupancyKindApi.Unknown }, + old: { ...housing, occupancy: Occupancy.UNKNOWN }, new: { ...housing, - occupancy: OccupancyKindApi.Unknown, + occupancy: Occupancy.UNKNOWN, status: HousingStatusApi.Completed, subStatus: 'Sortie de la vacance' }, diff --git a/server/src/scripts/import-lovac/source-housings/source-housing-processor.ts b/server/src/scripts/import-lovac/source-housings/source-housing-processor.ts index 86c67543b..9ddcaa077 100644 --- a/server/src/scripts/import-lovac/source-housings/source-housing-processor.ts +++ b/server/src/scripts/import-lovac/source-housings/source-housing-processor.ts @@ -1,7 +1,7 @@ import { WritableStream } from 'node:stream/web'; import { v4 as uuidv4 } from 'uuid'; -import { AddressKinds } from '@zerologementvacant/models'; +import { AddressKinds, Occupancy } from '@zerologementvacant/models'; import { ReporterError, ReporterOptions } from '~/scripts/import-lovac/infra'; import { SourceHousing } from '~/scripts/import-lovac/source-housings/source-housing'; import { createLogger } from '~/infra/logger'; @@ -9,7 +9,6 @@ import { HousingApi, HousingId, normalizeDataFileYears, - OccupancyKindApi, OwnershipKindsApi } from '~/models/HousingApi'; import { HousingStatusApi } from '~/models/HousingStatusApi'; @@ -95,8 +94,8 @@ export function createSourceHousingProcessor(opts: ProcessorOptions) { buildingLocation: chunk.location_detail, ownershipKind: chunk.condominium as OwnershipKindsApi, status: HousingStatusApi.NeverContacted, - occupancy: OccupancyKindApi.Vacant, - occupancyRegistered: OccupancyKindApi.Vacant, + occupancy: Occupancy.VACANT, + occupancyRegistered: Occupancy.VACANT, source: 'lovac' }; await housingRepository.insert(housing); @@ -231,7 +230,7 @@ function applyChanges( if (rules.some((rule) => rule())) { return { - occupancy: OccupancyKindApi.Vacant, + occupancy: Occupancy.VACANT, status: HousingStatusApi.NeverContacted, subStatus: null }; diff --git a/server/src/scripts/import-lovac/source-housings/test/source-housing-command.test.ts b/server/src/scripts/import-lovac/source-housings/test/source-housing-command.test.ts index d1048b0e4..aea1c8637 100644 --- a/server/src/scripts/import-lovac/source-housings/test/source-housing-command.test.ts +++ b/server/src/scripts/import-lovac/source-housings/test/source-housing-command.test.ts @@ -19,7 +19,7 @@ import { genHousingApi, genUserApi } from '~/test/testFixtures'; -import { HousingApi, OccupancyKindApi } from '~/models/HousingApi'; +import { HousingApi } from '~/models/HousingApi'; import { Establishments, formatEstablishmentApi @@ -27,6 +27,7 @@ import { import { UserApi } from '~/models/UserApi'; import config from '~/infra/config'; import { formatUserApi, Users } from '~/repositories/userRepository'; +import { Occupancy } from '@zerologementvacant/models'; describe('Source housing command', () => { const command = createSourceHousingCommand(); @@ -57,13 +58,13 @@ describe('Source housing command', () => { ...genHousingApi(), geoCode: sourceHousing.geo_code, localId: sourceHousing.local_id, - occupancy: OccupancyKindApi.Rent + occupancy: Occupancy.RENT })), ...vacantHousings.map((sourceHousing) => ({ ...genHousingApi(), geoCode: sourceHousing.geo_code, localId: sourceHousing.local_id, - occupancy: OccupancyKindApi.Vacant, + occupancy: Occupancy.VACANT, dataFileYears: ['lovac-2022'] })) ]; @@ -83,7 +84,7 @@ describe('Source housing command', () => { ); expect(actual).toHaveLength(missingSourceHousings.length); expect(actual).toSatisfyAll((housing) => { - return housing.occupancy === OccupancyKindApi.Vacant; + return housing.occupancy === Occupancy.VACANT; }); expect(actual).toSatisfyAll((housing) => { return housing.status === HousingStatusApi.NeverContacted; @@ -103,7 +104,7 @@ describe('Source housing command', () => { return housing.data_file_years?.includes('lovac-2024') ?? false; }); expect(actual).toSatisfyAll((housing) => { - return housing.occupancy === OccupancyKindApi.Vacant; + return housing.occupancy === Occupancy.VACANT; }); expect(actual).toSatisfyAll((housing) => { return housing.status === HousingStatusApi.NeverContacted; diff --git a/server/src/scripts/shared/models/DatafoncierHousing.ts b/server/src/scripts/shared/models/DatafoncierHousing.ts index 61654130a..890f13756 100644 --- a/server/src/scripts/shared/models/DatafoncierHousing.ts +++ b/server/src/scripts/shared/models/DatafoncierHousing.ts @@ -1,13 +1,13 @@ import fp from 'lodash/fp'; -import { - HousingRecordApi, - OccupancyKindApi, - OwnershipKindsApi -} from '~/models/HousingApi'; +import { HousingRecordApi, OwnershipKindsApi } from '~/models/HousingApi'; import { v4 as uuidv4 } from 'uuid'; import { ReferenceDataYear } from '~/repositories/housingRepository'; import { HousingStatusApi } from '~/models/HousingStatusApi'; -import { DatafoncierHousing, HousingSource } from '@zerologementvacant/models'; +import { + DatafoncierHousing, + HousingSource, + toOccupancy +} from '@zerologementvacant/models'; import { parse } from 'date-fns'; export const toHousingRecordApi = fp.curry( @@ -36,8 +36,8 @@ export const toHousingRecordApi = fp.curry( buildingLocation: `${housing.dnubat}${housing.descc}${housing.dniv}${housing.dpor}`, ownershipKind: housing.ctpdl as OwnershipKindsApi, status: HousingStatusApi.NeverContacted, - occupancy: housing.ccthp as OccupancyKindApi, - occupancyRegistered: housing.ccthp as OccupancyKindApi, + occupancy: toOccupancy(housing.ccthp), + occupancyRegistered: toOccupancy(housing.ccthp), source: additionalData.source, mutationDate: parse(housing.jdatatv, 'ddMMyyyy', new Date()) }; diff --git a/server/src/test/testFixtures.ts b/server/src/test/testFixtures.ts index 56280bc7b..2c22e6926 100644 --- a/server/src/test/testFixtures.ts +++ b/server/src/test/testFixtures.ts @@ -17,11 +17,7 @@ import { hasPriority, INTENTS } from '~/models/EstablishmentApi'; -import { - ENERGY_CONSUMPTION_GRADES, - HousingApi, - OccupancyKindApi -} from '~/models/HousingApi'; +import { ENERGY_CONSUMPTION_GRADES, HousingApi } from '~/models/HousingApi'; import { CampaignApi } from '~/models/CampaignApi'; import { GeoPerimeterApi } from '~/models/GeoPerimeterApi'; import { ProspectApi } from '~/models/ProspectApi'; @@ -75,6 +71,8 @@ import { HOUSING_SOURCE_VALUES, INTERNAL_CO_CONDOMINIUM_VALUES, INTERNAL_MONO_CONDOMINIUM_VALUES, + Occupancy, + OCCUPANCY_VALUES, UserAccountDTO } from '@zerologementvacant/models'; @@ -283,10 +281,10 @@ export const genBuildingApi = (housingList: HousingApi[]): BuildingApi => { uuidv4(), housingCount: housingList.length, vacantHousingCount: housingList.filter( - (housing) => housing.occupancy === OccupancyKindApi.Vacant + (housing) => housing.occupancy === Occupancy.VACANT ).length, rentHousingCount: housingList.filter( - (housing) => housing.occupancy === OccupancyKindApi.Rent + (housing) => housing.occupancy === Occupancy.RENT ).length }; }; @@ -360,10 +358,8 @@ export const genHousingApi = ( })) ]), energyConsumption: faker.helpers.arrayElement(ENERGY_CONSUMPTION_GRADES), - occupancy: faker.helpers.arrayElement(Object.values(OccupancyKindApi)), - occupancyRegistered: faker.helpers.arrayElement( - Object.values(OccupancyKindApi) - ), + occupancy: faker.helpers.arrayElement(OCCUPANCY_VALUES), + occupancyRegistered: faker.helpers.arrayElement(OCCUPANCY_VALUES), buildingVacancyRate: faker.number.float(), campaignIds: [], contactCount: genNumber(1), From bb4a6276599ec9b93a06bc7f626357a1dc0d9c80 Mon Sep 17 00:00:00 2001 From: Andrea Gueugnaut Date: Wed, 27 Nov 2024 16:51:45 +0100 Subject: [PATCH 05/11] test: improve datafoncier housing tests --- frontend/src/mocks/handlers/housing-handlers.ts | 2 +- packages/models/src/test/fixtures.ts | 2 +- server/src/controllers/housingController.test.ts | 5 +++++ server/src/test/testFixtures.ts | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/src/mocks/handlers/housing-handlers.ts b/frontend/src/mocks/handlers/housing-handlers.ts index 62727727c..7fa4be7aa 100644 --- a/frontend/src/mocks/handlers/housing-handlers.ts +++ b/frontend/src/mocks/handlers/housing-handlers.ts @@ -86,7 +86,7 @@ export const housingHandlers: RequestHandler[] = [ // Add a housing http.post( - `${config.apiEndpoint}/api/housing/creation`, + `${config.apiEndpoint}/api/housing`, async ({ request }) => { const payload = await request.json(); const datafoncierHousing = data.datafoncierHousings.find( diff --git a/packages/models/src/test/fixtures.ts b/packages/models/src/test/fixtures.ts index 3ef992cff..2a8930af5 100644 --- a/packages/models/src/test/fixtures.ts +++ b/packages/models/src/test/fixtures.ts @@ -127,7 +127,7 @@ export function genDatafoncierHousingDTO( dloy48a: faker.number.int(99), top48a: faker.string.alphanumeric(1), dnatlc: faker.string.alphanumeric(1), - ccthp: faker.helpers.arrayElement(['L', 'V']), + ccthp: faker.helpers.arrayElement(OCCUPANCY_VALUES), proba_rprs: faker.string.alphanumeric(7), typeact: faker.string.alphanumeric(4), loghvac: faker.string.alphanumeric(1), diff --git a/server/src/controllers/housingController.test.ts b/server/src/controllers/housingController.test.ts index 809b93b1c..2aaeafc7c 100644 --- a/server/src/controllers/housingController.test.ts +++ b/server/src/controllers/housingController.test.ts @@ -283,11 +283,16 @@ describe('Housing API', () => { `${housingOwnersTable}.housing_id` ) .where(`${housingTable}.local_id`, datafoncierHousing.idlocal); + expect(actualOwners.length).toBe(datafoncierOwners.length); expect(actualOwners).toSatisfyAll((actualOwner) => { return existingOwners.some((existingOwner) => { return existingOwner.id === actualOwner.id; }); }); + const actualHousing = await Housing() + .where({ local_id: datafoncierHousing.idlocal }) + .first(); + expect(actualHousing).toBeDefined(); }); it('should assign its owners', async () => { diff --git a/server/src/test/testFixtures.ts b/server/src/test/testFixtures.ts index 2c22e6926..415673a8c 100644 --- a/server/src/test/testFixtures.ts +++ b/server/src/test/testFixtures.ts @@ -695,7 +695,7 @@ export const genDatafoncierHousing = ( dloy48a: genNumber(2), top48a: randomstring.generate(1), dnatlc: randomstring.generate(1), - ccthp: oneOf(['L', 'V']), + ccthp: faker.helpers.arrayElement([...OCCUPANCY_VALUES, null]), proba_rprs: randomstring.generate(7), typeact: randomstring.generate(4), loghvac: randomstring.generate(1), From 749e23bd6015b927bdcab365aa7cb6fd06b03214 Mon Sep 17 00:00:00 2001 From: Andrea Gueugnaut Date: Wed, 27 Nov 2024 17:43:50 +0100 Subject: [PATCH 06/11] test: fix DatafoncierHousing fixture --- packages/models/src/test/fixtures.ts | 9 +++++++-- server/src/test/testFixtures.ts | 7 ++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/models/src/test/fixtures.ts b/packages/models/src/test/fixtures.ts index 2a8930af5..b84bafbcd 100644 --- a/packages/models/src/test/fixtures.ts +++ b/packages/models/src/test/fixtures.ts @@ -11,7 +11,7 @@ import { OwnerDTO } from '../OwnerDTO'; import { RolesDTO } from '../RolesDTO'; import { SenderDTO, SignatoryDTO } from '../SenderDTO'; import { UserDTO } from '../UserDTO'; -import { OCCUPANCY_VALUES } from '../Occupancy'; +import { Occupancy, OCCUPANCY_VALUES } from '../Occupancy'; import { HOUSING_KIND_VALUES } from '../HousingKind'; import { DatafoncierHousing } from '../DatafoncierHousing'; import { HOUSING_STATUS_VALUES } from '../HousingStatus'; @@ -127,7 +127,12 @@ export function genDatafoncierHousingDTO( dloy48a: faker.number.int(99), top48a: faker.string.alphanumeric(1), dnatlc: faker.string.alphanumeric(1), - ccthp: faker.helpers.arrayElement(OCCUPANCY_VALUES), + ccthp: faker.helpers.arrayElement([ + ...OCCUPANCY_VALUES.filter( + (occupancy) => occupancy !== Occupancy.UNKNOWN + ), + null + ]), proba_rprs: faker.string.alphanumeric(7), typeact: faker.string.alphanumeric(4), loghvac: faker.string.alphanumeric(1), diff --git a/server/src/test/testFixtures.ts b/server/src/test/testFixtures.ts index 415673a8c..12bb1d27c 100644 --- a/server/src/test/testFixtures.ts +++ b/server/src/test/testFixtures.ts @@ -695,7 +695,12 @@ export const genDatafoncierHousing = ( dloy48a: genNumber(2), top48a: randomstring.generate(1), dnatlc: randomstring.generate(1), - ccthp: faker.helpers.arrayElement([...OCCUPANCY_VALUES, null]), + ccthp: faker.helpers.arrayElement([ + ...OCCUPANCY_VALUES.filter( + (occupancy) => occupancy !== Occupancy.UNKNOWN + ), + null + ]), proba_rprs: randomstring.generate(7), typeact: randomstring.generate(4), loghvac: randomstring.generate(1), From 7fc504c075a64ef33f369f34d50dc53154688403 Mon Sep 17 00:00:00 2001 From: Andrea Gueugnaut Date: Wed, 27 Nov 2024 19:11:20 +0100 Subject: [PATCH 07/11] refactor(frontend): improve getSource readability --- frontend/src/models/Housing.tsx | 45 ++++++++++++------------ frontend/src/models/test/Housing.test.ts | 16 +++++---- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/frontend/src/models/Housing.tsx b/frontend/src/models/Housing.tsx index cbb465574..60fc5d542 100644 --- a/frontend/src/models/Housing.tsx +++ b/frontend/src/models/Housing.tsx @@ -1,4 +1,5 @@ import { differenceInDays, format } from 'date-fns'; +import { List } from 'immutable'; import { match, Pattern } from 'ts-pattern'; import { @@ -236,37 +237,37 @@ export const getOccupancy = ( ) => (occupancy && occupancy.length > 0 ? occupancy : OccupancyUnknown); export function getSource(housing: Housing): string { - const labels: Record = { - 'lovac': 'LOVAC', - 'ff': 'Fichiers fonciers', + lovac: 'LOVAC', + ff: 'Fichiers fonciers', 'datafoncier-import': 'Fichiers fonciers', 'datafoncier-manual': 'Fichiers fonciers' }; - const aggregatedData: Record = {}; + const years = List(housing.dataFileYears) + .groupBy((dataFileYear) => dataFileYear.split('-').at(0) as string) + .map((dataFileYears) => + dataFileYears.map( + (dataFileYear) => dataFileYear.split('-').at(1) as string + ) + ) + .filter((years) => !years.isEmpty()) + .reduce((acc, years, source) => { + const label = labels[source] ?? source; + return acc.concat(`${label} (${years.join(', ')})`); + }, List()); + + const source = housing.source ? labels[housing.source] : null; - let result = null; - if(housing.dataFileYears.length > 0) { - housing.dataFileYears.forEach((item) => { - const [name, year] = item.split('-'); - if (!aggregatedData[name]) { - aggregatedData[name] = []; - } - aggregatedData[name].push(year); - }); + if (!years.isEmpty()) { + return years.join(', '); + } - result = Object.keys(aggregatedData) - .map((name) => { - const years = aggregatedData[name].join(', '); - return labels[name] + ' (' + years + ')'; - }) - .join(', '); - } else if (housing.source) { - result = labels[housing.source]; + if (source) { + return source; } - return result || 'Inconnue'; + return 'Inconnue'; } export function toHousingDTO(housing: Housing): HousingDTO { diff --git a/frontend/src/models/test/Housing.test.ts b/frontend/src/models/test/Housing.test.ts index e4ea73226..ddfe48c1e 100644 --- a/frontend/src/models/test/Housing.test.ts +++ b/frontend/src/models/test/Housing.test.ts @@ -56,14 +56,16 @@ describe('Housing', () => { describe('getSource', () => { it.each` - dataFileYears | expected - ${['lovac-2019']} | ${'LOVAC (2019)'} - ${['lovac-2020', 'lovac-2021']} | ${'LOVAC (2020, 2021)'} - ${['ff-2020', 'ff-2021', 'lovac-2021']} | ${'Fichiers fonciers (2020, 2021), LOVAC (2021)'} + dataFileYears | source | expected + ${['lovac-2019']} | ${'lovac'} | ${'LOVAC (2019)'} + ${['lovac-2020', 'lovac-2021']} | ${'lovac'} | ${'LOVAC (2020, 2021)'} + ${['ff-2020', 'ff-2021', 'lovac-2021']} | ${'lovac'} | ${'Fichiers fonciers (2020, 2021), LOVAC (2021)'} + ${[]} | ${'datafoncier-manual'} | ${'Fichiers fonciers'} + ${[]} | ${'datafoncier-import'} | ${'Fichiers fonciers'} `( - 'should format $dataFileYears to $expected', - ({ dataFileYears, expected }) => { - const housing: Housing = { ...genHousing(), dataFileYears }; + `should format $dataFileYears and $source to $expected`, + ({ dataFileYears, source, expected }) => { + const housing: Housing = { ...genHousing(), dataFileYears, source }; const actual = getSource(housing); From 94655add2e2423a5d69a196ef56b06d6ad338e49 Mon Sep 17 00:00:00 2001 From: Andrea Gueugnaut Date: Wed, 27 Nov 2024 19:27:42 +0100 Subject: [PATCH 08/11] fix(server): correctly set data_years and data_file_years when adding a housing --- server/src/scripts/shared/models/DatafoncierHousing.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server/src/scripts/shared/models/DatafoncierHousing.ts b/server/src/scripts/shared/models/DatafoncierHousing.ts index 890f13756..24e1761c9 100644 --- a/server/src/scripts/shared/models/DatafoncierHousing.ts +++ b/server/src/scripts/shared/models/DatafoncierHousing.ts @@ -1,7 +1,6 @@ import fp from 'lodash/fp'; import { HousingRecordApi, OwnershipKindsApi } from '~/models/HousingApi'; import { v4 as uuidv4 } from 'uuid'; -import { ReferenceDataYear } from '~/repositories/housingRepository'; import { HousingStatusApi } from '~/models/HousingStatusApi'; import { DatafoncierHousing, @@ -31,8 +30,11 @@ export const toHousingRecordApi = fp.curry( livingArea: housing.stoth, buildingYear: housing.jannath, taxed: false, - dataYears: [ReferenceDataYear + 1], - dataFileYears: [`${ReferenceDataYear + 1}`], + // The data in `df_housing_nat` and `df_owners_nat` is from 2023 + dataYears: [2023], + dataFileYears: [ + `${additionalData.source === 'lovac' ? 'lovac' : 'ff'}-2023` + ], buildingLocation: `${housing.dnubat}${housing.descc}${housing.dniv}${housing.dpor}`, ownershipKind: housing.ctpdl as OwnershipKindsApi, status: HousingStatusApi.NeverContacted, From 5b0a7d6b108938b9f7ce2a6b1b1cb86869150e6c Mon Sep 17 00:00:00 2001 From: Andrea Gueugnaut Date: Thu, 28 Nov 2024 09:41:19 +0100 Subject: [PATCH 09/11] test(frontend): test housing details display --- .../HousingDetailsSubCardOccupancy.tsx | 9 +--- .../views/Housing/test/HousingView.test.tsx | 45 +++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/HousingDetails/HousingDetailsSubCardOccupancy.tsx b/frontend/src/components/HousingDetails/HousingDetailsSubCardOccupancy.tsx index 339abca34..70e69d4fd 100644 --- a/frontend/src/components/HousingDetails/HousingDetailsSubCardOccupancy.tsx +++ b/frontend/src/components/HousingDetails/HousingDetailsSubCardOccupancy.tsx @@ -30,10 +30,7 @@ function HousingDetailsCardOccupancy({ housing, lastOccupancyEvent }: Props) { ? housing.vacancyStartYear : undefined; - function situationSince( - occupancy: string, - lastOccupancyChange: number | undefined - ): string { + function situationSince(lastOccupancyChange: number | undefined): string { if (lastOccupancyChange === undefined) { return 'Inconnu'; } @@ -77,9 +74,7 @@ function HousingDetailsCardOccupancy({ housing, lastOccupancyEvent }: Props) { Dans cette situation depuis - - {situationSince(housing.occupancy, lastOccupancyChange)} - + {situationSince(lastOccupancyChange)} Source diff --git a/frontend/src/views/Housing/test/HousingView.test.tsx b/frontend/src/views/Housing/test/HousingView.test.tsx index 4fe2c32c7..4ebe2d2e5 100644 --- a/frontend/src/views/Housing/test/HousingView.test.tsx +++ b/frontend/src/views/Housing/test/HousingView.test.tsx @@ -7,6 +7,7 @@ import { createMemoryRouter, RouterProvider } from 'react-router-dom'; import { HousingDTO, HousingOwnerDTO, + Occupancy, OwnerDTO } from '@zerologementvacant/models'; import { @@ -62,6 +63,50 @@ describe('Housing view', () => { expect(name).toBeVisible(); }); + describe('Show housing details', () => { + describe('Vacancy start year', () => { + it('should be unknown', async () => { + housing.occupancy = Occupancy.RENT; + housing.vacancyStartYear = undefined; + + renderView(housing); + + const vacancyStartYear = await screen + .findByText(/^Dans cette situation depuis/) + // eslint-disable-next-line testing-library/no-node-access + .then((label) => label.nextElementSibling); + expect(vacancyStartYear).toHaveTextContent('Inconnu'); + }); + + it('should be defined', async () => { + housing.occupancy = Occupancy.VACANT; + housing.vacancyStartYear = faker.date.past().getFullYear(); + + renderView(housing); + + const vacancyStartYear = await screen + .findByText(/^Dans cette situation depuis/) + // eslint-disable-next-line testing-library/no-node-access + .then((label) => label.nextElementSibling); + expect(vacancyStartYear).toHaveTextContent('Moins d’un an'); + }); + }); + + describe('Source', () => { + it('should be "Fichiers fonciers (2023)"', async () => { + housing.dataFileYears = ['ff-2023']; + + renderView(housing); + + const source = await screen + .findByText(/^Source/) + // eslint-disable-next-line testing-library/no-node-access + .then((label) => label.nextElementSibling); + expect(source).toHaveTextContent('Fichiers fonciers (2023)'); + }); + }); + }); + describe('Update owner details', () => { it('should update their name', async () => { renderView(housing); From 33b73e41095c95fb91a9b59cb9e7d5bedb48f4ba Mon Sep 17 00:00:00 2001 From: Andrea Gueugnaut Date: Thu, 28 Nov 2024 10:06:36 +0100 Subject: [PATCH 10/11] test(server): recreate df_housing_nat before tests --- .../database/seeds/test/df-housing-nat.ts | 259 +++++++++--------- 1 file changed, 130 insertions(+), 129 deletions(-) diff --git a/server/src/infra/database/seeds/test/df-housing-nat.ts b/server/src/infra/database/seeds/test/df-housing-nat.ts index d47861a52..70750b5f6 100644 --- a/server/src/infra/database/seeds/test/df-housing-nat.ts +++ b/server/src/infra/database/seeds/test/df-housing-nat.ts @@ -6,136 +6,137 @@ const DF_HOUSING_NAT = 'df_housing_nat'; export async function seed(knex: Knex): Promise { const exists = await knex.schema.hasTable(DF_HOUSING_NAT); - if (!exists) { - await knex.schema.createTable(DF_HOUSING_NAT, (table) => { - table.string('idlocal').notNullable(); - table.string('idbat').notNullable(); - table.string('idpar').notNullable(); - table.string('idtup').notNullable(); - table.string('idsec').notNullable(); - table.string('idvoie').notNullable(); - table.string('idprocpte').notNullable(); - table.string('idcom').notNullable(); - table.string('idcomtxt').notNullable(); - table.string('ccodep').notNullable(); - table.string('ccodir').notNullable(); - table.string('ccocom').notNullable(); - table.string('invar').notNullable(); - table.string('ccopre'); - table.string('ccosec').notNullable(); - table.string('dnupla').notNullable(); - table.string('dnubat').notNullable(); - table.string('descc').notNullable(); - table.string('dniv').notNullable(); - table.string('dpor').notNullable(); - table.string('ccoriv').notNullable(); - table.string('ccovoi').notNullable(); - table.string('dnvoiri').notNullable(); - table.string('dindic'); - table.string('ccocif').notNullable(); - table.string('dvoilib').notNullable(); - table.string('cleinvar').notNullable(); - table.string('ccpper').notNullable(); - table.string('gpdl').notNullable(); - table.string('ctpdl'); - table.string('dnupro').notNullable(); - table.string('jdatat').notNullable(); - table.string('jdatatv').notNullable(); - table.integer('jdatatan').notNullable(); - table.string('dnufnl'); - table.string('ccoeva').notNullable(); - table.string('ccoevatxt').notNullable(); - table.string('dteloc').notNullable(); - table.string('dteloctxt').notNullable(); - table.string('logh').notNullable(); - table.string('loghmais').notNullable(); - table.string('loghappt'); - table.string('gtauom').notNullable(); - table.string('dcomrd').notNullable(); - table.string('ccoplc'); - table.string('ccoplctxt'); - table.string('cconlc').notNullable(); - table.string('cconlctxt').notNullable(); - table.integer('dvltrt').notNullable(); - table.string('cc48lc'); - table.integer('dloy48a'); - table.string('top48a').notNullable(); - table.string('dnatlc').notNullable(); - table.string('ccthp').notNullable(); - table.string('proba_rprs').notNullable(); - table.string('typeact'); - table.string('loghvac'); - table.string('loghvac2a'); - table.string('loghvac5a'); - table.string('loghvacdeb'); - table.string('cchpr'); - table.string('jannat').notNullable(); - table.string('dnbniv').notNullable(); - table.integer('nbetagemax').notNullable(); - table.integer('nbnivssol'); - table.string('hlmsem'); - table.string('loghlls').notNullable(); - table.string('postel'); - table.string('dnatcg'); - table.string('jdatcgl').notNullable(); - table.integer('fburx').notNullable(); - table.string('gimtom'); - table.string('cbtabt'); - table.string('jdbabt'); - table.string('jrtabt'); - table.string('cconac'); - table.string('cconactxt'); - table.string('toprev').notNullable(); - table.integer('ccoifp').notNullable(); - table.integer('jannath').notNullable(); - table.integer('janbilmin').notNullable(); - table.integer('npevph').notNullable(); - table.integer('stoth').notNullable(); - table.integer('stotdsueic').notNullable(); - table.integer('npevd').notNullable(); - table.integer('stotd').notNullable(); - table.integer('npevp').notNullable(); - table.integer('sprincp').notNullable(); - table.integer('ssecp').notNullable(); - table.integer('ssecncp').notNullable(); - table.integer('sparkp').notNullable(); - table.integer('sparkncp').notNullable(); - table.integer('npevtot').notNullable(); - table.integer('slocal').notNullable(); - table.integer('npiece_soc').notNullable(); - table.integer('npiece_ff').notNullable(); - table.integer('npiece_i').notNullable(); - table.integer('npiece_p2').notNullable(); - table.integer('nbannexe').notNullable(); - table.integer('nbgarpark').notNullable(); - table.integer('nbagrement').notNullable(); - table.integer('nbterrasse').notNullable(); - table.integer('nbpiscine').notNullable(); - table.integer('ndroit').notNullable(); - table.integer('ndroitindi').notNullable(); - table.integer('ndroitpro').notNullable(); - table.integer('ndroitges').notNullable(); - table.string('catpro2').notNullable(); - table.string('catpro2txt').notNullable(); - table.string('catpro3').notNullable(); - table.string('catpropro2').notNullable(); - table.string('catproges2').notNullable(); - table.string('locprop').notNullable(); - table.string('locproptxt').notNullable(); - table.string('source_geo').notNullable(); - table.string('vecteur').notNullable(); - table.string('ban_id').notNullable(); - table.string('ban_geom'); - table.string('ban_type').notNullable(); - table.string('ban_score').notNullable(); - table.string('ban_cp').notNullable(); - table.string('code_epci'); - table.string('lib_epci'); - table.string('geomloc'); - table.integer('idpk'); - table.integer('dis_ban_ff').notNullable(); - }); + if (exists) { + await knex.schema.dropTable(DF_HOUSING_NAT); } + await knex.schema.createTable(DF_HOUSING_NAT, (table) => { + table.string('idlocal').notNullable(); + table.string('idbat').notNullable(); + table.string('idpar').notNullable(); + table.string('idtup').notNullable(); + table.string('idsec').notNullable(); + table.string('idvoie').notNullable(); + table.string('idprocpte').notNullable(); + table.string('idcom').notNullable(); + table.string('idcomtxt').notNullable(); + table.string('ccodep').notNullable(); + table.string('ccodir').notNullable(); + table.string('ccocom').notNullable(); + table.string('invar').notNullable(); + table.string('ccopre'); + table.string('ccosec').notNullable(); + table.string('dnupla').notNullable(); + table.string('dnubat').notNullable(); + table.string('descc').notNullable(); + table.string('dniv').notNullable(); + table.string('dpor').notNullable(); + table.string('ccoriv').notNullable(); + table.string('ccovoi').notNullable(); + table.string('dnvoiri').notNullable(); + table.string('dindic'); + table.string('ccocif').notNullable(); + table.string('dvoilib').notNullable(); + table.string('cleinvar').notNullable(); + table.string('ccpper').notNullable(); + table.string('gpdl').notNullable(); + table.string('ctpdl'); + table.string('dnupro').notNullable(); + table.string('jdatat').notNullable(); + table.string('jdatatv').notNullable(); + table.integer('jdatatan').notNullable(); + table.string('dnufnl'); + table.string('ccoeva').notNullable(); + table.string('ccoevatxt').notNullable(); + table.string('dteloc').notNullable(); + table.string('dteloctxt').notNullable(); + table.string('logh').notNullable(); + table.string('loghmais').notNullable(); + table.string('loghappt'); + table.string('gtauom').notNullable(); + table.string('dcomrd').notNullable(); + table.string('ccoplc'); + table.string('ccoplctxt'); + table.string('cconlc').notNullable(); + table.string('cconlctxt').notNullable(); + table.integer('dvltrt').notNullable(); + table.string('cc48lc'); + table.integer('dloy48a'); + table.string('top48a').notNullable(); + table.string('dnatlc').notNullable(); + table.string('ccthp').notNullable(); + table.string('proba_rprs').notNullable(); + table.string('typeact'); + table.string('loghvac'); + table.string('loghvac2a'); + table.string('loghvac5a'); + table.string('loghvacdeb'); + table.string('cchpr'); + table.string('jannat').notNullable(); + table.string('dnbniv').notNullable(); + table.integer('nbetagemax').notNullable(); + table.integer('nbnivssol'); + table.string('hlmsem'); + table.string('loghlls').notNullable(); + table.string('postel'); + table.string('dnatcg'); + table.string('jdatcgl').notNullable(); + table.integer('fburx').notNullable(); + table.string('gimtom'); + table.string('cbtabt'); + table.string('jdbabt'); + table.string('jrtabt'); + table.string('cconac'); + table.string('cconactxt'); + table.string('toprev').notNullable(); + table.integer('ccoifp').notNullable(); + table.integer('jannath').notNullable(); + table.integer('janbilmin').notNullable(); + table.integer('npevph').notNullable(); + table.integer('stoth').notNullable(); + table.integer('stotdsueic').notNullable(); + table.integer('npevd').notNullable(); + table.integer('stotd').notNullable(); + table.integer('npevp').notNullable(); + table.integer('sprincp').notNullable(); + table.integer('ssecp').notNullable(); + table.integer('ssecncp').notNullable(); + table.integer('sparkp').notNullable(); + table.integer('sparkncp').notNullable(); + table.integer('npevtot').notNullable(); + table.integer('slocal').notNullable(); + table.integer('npiece_soc').notNullable(); + table.integer('npiece_ff').notNullable(); + table.integer('npiece_i').notNullable(); + table.integer('npiece_p2').notNullable(); + table.integer('nbannexe').notNullable(); + table.integer('nbgarpark').notNullable(); + table.integer('nbagrement').notNullable(); + table.integer('nbterrasse').notNullable(); + table.integer('nbpiscine').notNullable(); + table.integer('ndroit').notNullable(); + table.integer('ndroitindi').notNullable(); + table.integer('ndroitpro').notNullable(); + table.integer('ndroitges').notNullable(); + table.string('catpro2').notNullable(); + table.string('catpro2txt').notNullable(); + table.string('catpro3').notNullable(); + table.string('catpropro2').notNullable(); + table.string('catproges2').notNullable(); + table.string('locprop').notNullable(); + table.string('locproptxt').notNullable(); + table.string('source_geo').notNullable(); + table.string('vecteur').notNullable(); + table.string('ban_id').notNullable(); + table.string('ban_geom'); + table.string('ban_type').notNullable(); + table.string('ban_score').notNullable(); + table.string('ban_cp').notNullable(); + table.string('code_epci'); + table.string('lib_epci'); + table.string('geomloc'); + table.integer('idpk'); + table.integer('dis_ban_ff').notNullable(); + }); // Deletes ALL existing entries await knex(DF_HOUSING_NAT).delete(); From e1822d590842668e4e97543f116f3d5b762504c3 Mon Sep 17 00:00:00 2001 From: Andrea Gueugnaut Date: Thu, 28 Nov 2024 10:14:04 +0100 Subject: [PATCH 11/11] test(server): ccthp is nullable in production --- server/src/infra/database/seeds/development/df-housing-nat.ts | 2 +- server/src/infra/database/seeds/test/df-housing-nat.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/infra/database/seeds/development/df-housing-nat.ts b/server/src/infra/database/seeds/development/df-housing-nat.ts index 54edc91fd..b689a238d 100644 --- a/server/src/infra/database/seeds/development/df-housing-nat.ts +++ b/server/src/infra/database/seeds/development/df-housing-nat.ts @@ -63,7 +63,7 @@ export async function seed(knex: Knex): Promise { table.integer('dloy48a'); table.string('top48a').notNullable(); table.string('dnatlc').notNullable(); - table.string('ccthp').notNullable(); + table.string('ccthp'); table.string('proba_rprs').notNullable(); table.string('typeact'); table.string('loghvac'); diff --git a/server/src/infra/database/seeds/test/df-housing-nat.ts b/server/src/infra/database/seeds/test/df-housing-nat.ts index 70750b5f6..14396b39d 100644 --- a/server/src/infra/database/seeds/test/df-housing-nat.ts +++ b/server/src/infra/database/seeds/test/df-housing-nat.ts @@ -63,7 +63,7 @@ export async function seed(knex: Knex): Promise { table.integer('dloy48a'); table.string('top48a').notNullable(); table.string('dnatlc').notNullable(); - table.string('ccthp').notNullable(); + table.string('ccthp'); table.string('proba_rprs').notNullable(); table.string('typeact'); table.string('loghvac');