-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add unified owners import command
- Loading branch information
Showing
8 changed files
with
469 additions
and
5 deletions.
There are no files selected for viewing
18 changes: 18 additions & 0 deletions
18
server/src/infra/database/migrations/20241223105911-owners-dept.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import type { Knex } from 'knex'; | ||
|
||
export async function up(knex: Knex): Promise<void> { | ||
await knex.schema.createTable('owners_dept', (table) => { | ||
table | ||
.uuid('owner_id') | ||
.notNullable() | ||
.references('id') | ||
.inTable('owners') | ||
.onUpdate('CASCADE') | ||
.onDelete('CASCADE'); | ||
table.string('owner_idpersonne').notNullable().unique(); | ||
}); | ||
} | ||
|
||
export async function down(knex: Knex): Promise<void> { | ||
await knex.schema.dropTable('owners_dept'); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { Readable } from 'node:stream'; | ||
import { ReadableStream } from 'node:stream/web'; | ||
|
||
import db from '~/infra/database'; | ||
|
||
export const departmentalOwnersTable = 'owners_dept'; | ||
export const DepartmentalOwners = (transaction = db) => | ||
transaction<DepartmentalOwnerDBO>(departmentalOwnersTable); | ||
|
||
export interface DepartmentalOwnerDBO { | ||
owner_id: string; | ||
owner_idpersonne: string; | ||
} | ||
|
||
function stream(): ReadableStream<DepartmentalOwnerDBO> { | ||
const query = DepartmentalOwners() | ||
.select(`${departmentalOwnersTable}.*`) | ||
.orderBy('owner_id') | ||
.stream(); | ||
|
||
return Readable.toWeb(query); | ||
} | ||
|
||
const departmentalOwnersRepository = { | ||
stream | ||
}; | ||
|
||
export default departmentalOwnersRepository; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { | ||
createProcessor, | ||
FindHousingOwnersOptions | ||
} from '~/scripts/import-unified-owners/processor'; | ||
import { AWAITING_RANK, HousingOwnerApi } from '~/models/HousingOwnerApi'; | ||
import { | ||
HousingOwners, | ||
housingOwnersTable | ||
} from '~/repositories/housingOwnerRepository'; | ||
import { | ||
Owners, | ||
ownerTable, | ||
parseHousingOwnerApi | ||
} from '~/repositories/ownerRepository'; | ||
import departmentalOwnersRepository from '~/repositories/departmentalOwnersRepository'; | ||
|
||
export default function createImportUnifiedOwnersCommand() { | ||
return async (): Promise<void> => { | ||
const processor = createProcessor({ | ||
findHousingOwners, | ||
updateHousingOwner, | ||
removeHousingOwner | ||
}); | ||
|
||
await departmentalOwnersRepository.stream().pipeTo(processor); | ||
}; | ||
} | ||
|
||
export async function findHousingOwners( | ||
options: FindHousingOwnersOptions | ||
): Promise<ReadonlyArray<HousingOwnerApi>> { | ||
const housingOwners = await HousingOwners() | ||
.select(`${housingOwnersTable}.*`) | ||
.join(ownerTable, `${ownerTable}.id`, `${housingOwnersTable}.owner_id`) | ||
.select(`${ownerTable}.*`) | ||
.where((where) => { | ||
where.where({ | ||
owner_id: options.nationalOwner, | ||
rank: AWAITING_RANK | ||
}); | ||
}) | ||
.orWhere((where) => { | ||
where | ||
.whereIn( | ||
'owner_id', | ||
Owners().select('id').where('idpersonne', options.departmentalOwner) | ||
) | ||
.where('rank', '>=', 1); | ||
}); | ||
|
||
return housingOwners.map(parseHousingOwnerApi); | ||
} | ||
|
||
export async function updateHousingOwner( | ||
housingOwner: HousingOwnerApi | ||
): Promise<void> { | ||
await HousingOwners().update({ rank: housingOwner.rank }).where({ | ||
owner_id: housingOwner.ownerId, | ||
housing_id: housingOwner.housingId, | ||
housing_geo_code: housingOwner.housingGeoCode | ||
}); | ||
} | ||
|
||
export async function removeHousingOwner( | ||
housingOwner: HousingOwnerApi | ||
): Promise<void> { | ||
await HousingOwners().delete().where({ | ||
owner_id: housingOwner.ownerId, | ||
housing_id: housingOwner.housingId, | ||
housing_geo_code: housingOwner.housingGeoCode | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import createImportUnifiedOwnersCommand from '~/scripts/import-unified-owners/command'; | ||
|
||
const importer = createImportUnifiedOwnersCommand(); | ||
importer().catch((error) => { | ||
console.error(error); | ||
process.exit(1); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { WritableStream } from 'node:stream/web'; | ||
import { DepartmentalOwnerDBO } from '~/repositories/departmentalOwnersRepository'; | ||
import { createLogger } from '~/infra/logger'; | ||
import { AWAITING_RANK, HousingOwnerApi } from '~/models/HousingOwnerApi'; | ||
import { List } from 'immutable'; | ||
import async from 'async'; | ||
|
||
const logger = createLogger('processor'); | ||
|
||
export interface FindHousingOwnersOptions { | ||
/** | ||
* The national owner’s id. | ||
*/ | ||
nationalOwner: string; | ||
/** | ||
* The departmental owner’s idpersonne. | ||
*/ | ||
departmentalOwner: string; | ||
} | ||
|
||
export interface ProcessorOptions { | ||
findHousingOwners( | ||
options: FindHousingOwnersOptions | ||
): Promise<ReadonlyArray<HousingOwnerApi>>; | ||
updateHousingOwner(housingOwner: HousingOwnerApi): Promise<void>; | ||
removeHousingOwner(housingOwner: HousingOwnerApi): Promise<void>; | ||
} | ||
|
||
export function createProcessor(options: ProcessorOptions) { | ||
const { findHousingOwners, updateHousingOwner, removeHousingOwner } = options; | ||
|
||
return new WritableStream<DepartmentalOwnerDBO>({ | ||
async write(chunk): Promise<void> { | ||
try { | ||
logger.debug('Processing departmental owner...', { chunk }); | ||
|
||
const housingOwners = await findHousingOwners({ | ||
nationalOwner: chunk.owner_id, | ||
departmentalOwner: chunk.owner_idpersonne | ||
}); | ||
const pairs = toPairs(housingOwners); | ||
await async.forEach( | ||
pairs, | ||
async ([nationalOwner, departmentalOwner]) => { | ||
await removeHousingOwner(departmentalOwner); | ||
await updateHousingOwner({ | ||
...nationalOwner, | ||
rank: departmentalOwner.rank | ||
}); | ||
} | ||
); | ||
} catch (error) { | ||
// TODO | ||
} | ||
} | ||
}); | ||
} | ||
|
||
export function toPairs( | ||
housingOwners: ReadonlyArray<HousingOwnerApi> | ||
): [HousingOwnerApi, HousingOwnerApi][] { | ||
return List(housingOwners) | ||
.groupBy((housingOwner) => housingOwner.housingId) | ||
.filter( | ||
(housingOwners) => | ||
housingOwners.size === 2 && | ||
housingOwners.some( | ||
(housingOwner) => | ||
isNationalOwner(housingOwner) && housingOwner.rank === AWAITING_RANK | ||
) && | ||
housingOwners.some( | ||
(housingOwner) => | ||
isDepartmentalOwner(housingOwner) && housingOwner.rank >= 1 | ||
) | ||
) | ||
.map((housingOwners) => | ||
housingOwners.sortBy((housingOwner) => housingOwner.rank) | ||
) | ||
.toIndexedSeq() | ||
.map( | ||
(housingOwners) => | ||
housingOwners.toArray() as [HousingOwnerApi, HousingOwnerApi] | ||
) | ||
.toArray(); | ||
} | ||
|
||
export function isNationalOwner(housingOwner: HousingOwnerApi): boolean { | ||
return !housingOwner.idpersonne; | ||
} | ||
|
||
export function isDepartmentalOwner(housingOwner: HousingOwnerApi): boolean { | ||
return !isNationalOwner(housingOwner); | ||
} |
78 changes: 78 additions & 0 deletions
78
server/src/scripts/import-unified-owners/test/command.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import { faker } from '@faker-js/faker/locale/fr'; | ||
|
||
import createImportUnifiedOwnersCommand from '~/scripts/import-unified-owners/command'; | ||
import { | ||
formatHousingOwnerApi, | ||
HousingOwners | ||
} from '~/repositories/housingOwnerRepository'; | ||
import { AWAITING_RANK, HousingOwnerApi } from '~/models/HousingOwnerApi'; | ||
import { | ||
genHousingApi, | ||
genHousingOwnerApi, | ||
genOwnerApi | ||
} from '~/test/testFixtures'; | ||
import { formatOwnerApi, Owners } from '~/repositories/ownerRepository'; | ||
import { | ||
formatHousingRecordApi, | ||
Housing | ||
} from '~/repositories/housingRepository'; | ||
import { | ||
DepartmentalOwnerDBO, | ||
DepartmentalOwners | ||
} from '~/repositories/departmentalOwnersRepository'; | ||
import { OwnerApi } from '~/models/OwnerApi'; | ||
import { beforeAll } from '@jest/globals'; | ||
|
||
describe('Unified owners command', () => { | ||
describe('When a national owner is awaiting and a departmental owner is active', () => { | ||
const housing = genHousingApi(); | ||
const nationalOwner: OwnerApi = { | ||
...genOwnerApi(), | ||
idpersonne: undefined | ||
}; | ||
const existingDepartmentalOwner: OwnerApi = { | ||
...genOwnerApi(), | ||
idpersonne: faker.string.alphanumeric(8) | ||
}; | ||
const EXISTING_DEPARTMENTAL_OWNER_RANK = 1; | ||
const housingOwners: ReadonlyArray<HousingOwnerApi> = [ | ||
{ | ||
...genHousingOwnerApi(housing, existingDepartmentalOwner), | ||
rank: EXISTING_DEPARTMENTAL_OWNER_RANK | ||
}, | ||
{ ...genHousingOwnerApi(housing, nationalOwner), rank: AWAITING_RANK } | ||
]; | ||
const match: DepartmentalOwnerDBO = { | ||
owner_id: nationalOwner.id, | ||
owner_idpersonne: existingDepartmentalOwner.idpersonne as string | ||
}; | ||
|
||
beforeAll(async () => { | ||
await Housing().insert(formatHousingRecordApi(housing)); | ||
await Owners().insert( | ||
[nationalOwner, existingDepartmentalOwner].map(formatOwnerApi) | ||
); | ||
await DepartmentalOwners().insert(match); | ||
await HousingOwners().insert(housingOwners.map(formatHousingOwnerApi)); | ||
|
||
const command = createImportUnifiedOwnersCommand(); | ||
await command(); | ||
}); | ||
|
||
it('should remove the departmental housing owner', async () => { | ||
const actual = await HousingOwners() | ||
.where({ owner_id: existingDepartmentalOwner.id }) | ||
.first(); | ||
|
||
expect(actual).not.toBeDefined(); | ||
}); | ||
|
||
it('should replace the national housing owner by the previous departmental housing owner’s rank', async () => { | ||
const actual = await HousingOwners() | ||
.where({ owner_id: nationalOwner.id }) | ||
.first(); | ||
|
||
expect(actual!.rank).toBe(EXISTING_DEPARTMENTAL_OWNER_RANK); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.