Skip to content

Commit

Permalink
feat: add unified owners import command
Browse files Browse the repository at this point in the history
  • Loading branch information
Falinor committed Dec 24, 2024
1 parent 205ad89 commit 610150d
Show file tree
Hide file tree
Showing 8 changed files with 469 additions and 5 deletions.
18 changes: 18 additions & 0 deletions server/src/infra/database/migrations/20241223105911-owners-dept.ts
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');
}
13 changes: 8 additions & 5 deletions server/src/models/HousingOwnerApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,18 @@ export interface HousingOwnerApi extends OwnerApi {
locprop?: number;
}

type Incorrect = -1;
type Awaiting = -2;
export const AWAITING_RANK = -2 as const;
export const INCORRECT_RANK = -1 as const;
export const POSITIVE_RANKS = [1, 2, 3, 4, 5, 6] as const;

export type IncorrectRank = typeof INCORRECT_RANK;
export type AwaitingRank = typeof AWAITING_RANK;
export type PositiveRank = (typeof POSITIVE_RANKS)[number];
export type Rank = Incorrect | Awaiting | PositiveRank;
export function isIncorrect(rank: Rank): rank is Incorrect {
export type Rank = IncorrectRank | AwaitingRank | PositiveRank;
export function isIncorrect(rank: Rank): rank is IncorrectRank {
return rank === -1;
}
export function isAwaiting(rank: Rank): rank is Awaiting {
export function isAwaiting(rank: Rank): rank is AwaitingRank {
return rank === -2;
}

Expand Down
28 changes: 28 additions & 0 deletions server/src/repositories/departmentalOwnersRepository.ts
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;
72 changes: 72 additions & 0 deletions server/src/scripts/import-unified-owners/command.ts
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
});
}
7 changes: 7 additions & 0 deletions server/src/scripts/import-unified-owners/index.ts
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);
});
93 changes: 93 additions & 0 deletions server/src/scripts/import-unified-owners/processor.ts
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) {

Check failure on line 52 in server/src/scripts/import-unified-owners/processor.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

'error' is defined but never used
// 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 server/src/scripts/import-unified-owners/test/command.test.ts
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);
});
});
});
Loading

0 comments on commit 610150d

Please sign in to comment.