-
Notifications
You must be signed in to change notification settings - Fork 56
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[TECH] Rattrapage des dates de reconciliation (PIX-15026).
- Loading branch information
Showing
8 changed files
with
317 additions
and
0 deletions.
There are no files selected for viewing
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
39 changes: 39 additions & 0 deletions
39
api/scripts/certification/finishing-reconciled-at-migration.js
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,39 @@ | ||
import 'dotenv/config'; | ||
|
||
import * as url from 'node:url'; | ||
|
||
import { disconnect as disconnectFromDb } from '../../db/knex-database-connection.js'; | ||
import { usecases } from '../../src/certification/configuration/domain/usecases/index.js'; | ||
import { logger } from '../../src/shared/infrastructure/utils/logger.js'; | ||
|
||
/** | ||
* This migration re-perform the action from api/db/migrations/20240920163000_modify-to-reconciledAt-on-certification-candidates-table .js | ||
* | ||
* Usage: CHUNK_SIZE=100000 node scripts/certification/finishing-reconciled-at-migration.js | ||
**/ | ||
|
||
const modulePath = url.fileURLToPath(import.meta.url); | ||
const isLaunchedFromCommandLine = process.argv[1] === modulePath; | ||
|
||
const main = async ({ chunkSize }) => { | ||
return usecases.catchingUpCandidateReconciliation({ chunkSize }); | ||
}; | ||
|
||
(async () => { | ||
if (isLaunchedFromCommandLine) { | ||
let exitCode = 0; | ||
try { | ||
const chunkSize = parseInt(process.env.CHUNK_SIZE, 10); | ||
await main({ chunkSize: isNaN(chunkSize) ? undefined : chunkSize }); | ||
} catch (error) { | ||
logger.error(error); | ||
exitCode = 1; | ||
} finally { | ||
await disconnectFromDb(); | ||
// eslint-disable-next-line n/no-process-exit | ||
process.exit(exitCode); | ||
} | ||
} | ||
})(); | ||
|
||
export { main }; |
29 changes: 29 additions & 0 deletions
29
api/src/certification/configuration/domain/models/Candidate.js
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,29 @@ | ||
import Joi from 'joi'; | ||
|
||
import { EntityValidationError } from '../../../../shared/domain/errors.js'; | ||
|
||
export class Candidate { | ||
static #schema = Joi.object({ | ||
id: Joi.number().integer().required(), | ||
reconciledAt: Joi.date().required(), | ||
}); | ||
|
||
/** | ||
* @param {Object} props | ||
* @param {number} props.id | ||
* @param {Date} props.reconciledAt | ||
*/ | ||
constructor({ id, reconciledAt }) { | ||
this.id = id; | ||
this.reconciledAt = reconciledAt; | ||
|
||
this.#validate(); | ||
} | ||
|
||
#validate() { | ||
const { error } = Candidate.#schema.validate(this, { allowUnknown: false }); | ||
if (error) { | ||
throw EntityValidationError.fromJoiErrors(error.details); | ||
} | ||
} | ||
} |
44 changes: 44 additions & 0 deletions
44
api/src/certification/configuration/domain/usecases/catching-up-candidate-reconciliation.js
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,44 @@ | ||
/** | ||
* @typedef {import ('./index.js').CandidateRepository} CandidateRepository | ||
* @typedef {import ('../models/Candidate.js').Candidate} Candidate | ||
*/ | ||
|
||
import { DomainTransaction } from '../../../../shared/domain/DomainTransaction.js'; | ||
import { logger } from '../../../../shared/infrastructure/utils/logger.js'; | ||
|
||
/** | ||
* @param {Object} params | ||
* @param {number} params.[chunkSize] - default 100000 | ||
* @param {CandidateRepository} params.candidateRepository | ||
* @returns {Promise<void>} | ||
*/ | ||
export const catchingUpCandidateReconciliation = async ({ chunkSize = 100000, candidateRepository }) => { | ||
logger.info(`Starting certification-candidates.reconciledAt updates by chunk of ${chunkSize}`); | ||
let migratedLines = 0; | ||
let hasNext = true; | ||
do { | ||
const candidates = await candidateRepository.findCandidateWithoutReconciledAt({ limit: chunkSize }); | ||
logger.info(`Found ${candidates.length} certification-candidates without reconciledAt`); | ||
|
||
migratedLines += await performUpdates({ candidates, candidateRepository }); | ||
logger.info(`Update committed for ${migratedLines} candidates`); | ||
hasNext = !!candidates.length; | ||
} while (hasNext); | ||
|
||
logger.info(`Total of ${migratedLines} certification-candidates.reconciledAt updated`); | ||
}; | ||
|
||
/** | ||
* @param {Object} params | ||
* @param {Array<Candidate>} params.candidates | ||
* @param {CandidateRepository} params.candidateRepository | ||
*/ | ||
const performUpdates = async ({ candidates = [], candidateRepository }) => { | ||
return DomainTransaction.execute(async () => { | ||
let migratedLines = 0; | ||
for (const candidate of candidates) { | ||
migratedLines += await candidateRepository.update({ candidate }); | ||
} | ||
return migratedLines; | ||
}); | ||
}; |
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
48 changes: 48 additions & 0 deletions
48
api/src/certification/configuration/infrastructure/repositories/candidate-repository.js
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,48 @@ | ||
import { DomainTransaction } from '../../../../shared/domain/DomainTransaction.js'; | ||
import { Candidate } from '../../domain/models/Candidate.js'; | ||
|
||
/** | ||
* This function find candidates with a certification-course but no reconciledAt | ||
* | ||
* @param {Object} params | ||
* @param {number} params.[limit] - number of candidates to limit to | ||
* @returns {Array<Candidate>} - Candidates returned have a reconciledAt built from certification-course | ||
*/ | ||
export const findCandidateWithoutReconciledAt = async function ({ limit } = {}) { | ||
const knexConn = DomainTransaction.getConnection(); | ||
const data = await knexConn('certification-candidates') | ||
.select('certification-candidates.id', 'certification-courses.createdAt') | ||
.where((queryBuilder) => { | ||
queryBuilder.whereNotNull('certification-candidates.userId'); | ||
}) | ||
.andWhere((queryBuilder) => { | ||
queryBuilder.whereNull('certification-candidates.reconciledAt'); | ||
}) | ||
.innerJoin('certification-courses', function () { | ||
this.on('certification-courses.userId', 'certification-candidates.userId').andOn( | ||
'certification-courses.sessionId', | ||
'certification-candidates.sessionId', | ||
); | ||
}) | ||
.limit(limit); | ||
|
||
return data.map((data) => _toDomain({ id: data.id, reconciledAt: data.createdAt })); | ||
}; | ||
|
||
/** | ||
* @param {Object} params | ||
* @param {Candidate} params.candidate | ||
* @returns {number} - number of rows affected | ||
*/ | ||
export const update = async function ({ candidate }) { | ||
const knexConn = DomainTransaction.getConnection(); | ||
const results = await knexConn('certification-candidates') | ||
.update({ reconciledAt: candidate.reconciledAt }) | ||
.where({ id: candidate.id }); | ||
|
||
return results || 0; | ||
}; | ||
|
||
const _toDomain = ({ id, reconciledAt }) => { | ||
return new Candidate({ id, reconciledAt }); | ||
}; |
94 changes: 94 additions & 0 deletions
94
...cation/configuration/integration/infrastructure/repositories/candidate-repository_test.js
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,94 @@ | ||
import { Candidate } from '../../../../../../src/certification/configuration/domain/models/Candidate.js'; | ||
import * as candidateRepository from '../../../../../../src/certification/configuration/infrastructure/repositories/candidate-repository.js'; | ||
import { databaseBuilder, expect, knex } from '../../../../../test-helper.js'; | ||
|
||
describe('Certification | Configuration | Integration | Repository | candidate-repository', function () { | ||
describe('findCandidateWithoutReconciledAt', function () { | ||
it('should find candidate with a course and no reconciledAt', async function () { | ||
// given | ||
const courseDate = new Date(); | ||
const sessionId = databaseBuilder.factory.buildSession({}).id; | ||
const userId = databaseBuilder.factory.buildUser().id; | ||
databaseBuilder.factory.buildCertificationCourse({ userId, sessionId, createdAt: courseDate }); | ||
const candidateId = databaseBuilder.factory.buildCertificationCandidate({ | ||
userId, | ||
sessionId, | ||
reconciledAt: null, | ||
}).id; | ||
await databaseBuilder.commit(); | ||
// this is necessary to introduce the problem we want to find in the current database | ||
await knex('certification-candidates').update({ reconciledAt: null }).where({ userId }); | ||
|
||
// when | ||
const results = await candidateRepository.findCandidateWithoutReconciledAt(); | ||
|
||
// then | ||
expect(results).to.deep.equal([new Candidate({ id: candidateId, reconciledAt: courseDate })]); | ||
}); | ||
|
||
it('should find candidate with no course and no reconciledAt', async function () { | ||
// given | ||
const userId = databaseBuilder.factory.buildUser().id; | ||
const sessionId = databaseBuilder.factory.buildSession({ userId }).id; | ||
databaseBuilder.factory.buildCertificationCandidate({ | ||
userId, | ||
sessionId, | ||
reconciledAt: null, | ||
}); | ||
await databaseBuilder.commit(); | ||
// this is necessary to introduce the problem we want to find in the current database | ||
await knex('certification-candidates').update({ reconciledAt: null }).where({ userId }); | ||
|
||
// when | ||
const results = await candidateRepository.findCandidateWithoutReconciledAt(); | ||
|
||
// then | ||
expect(results).to.be.empty; | ||
}); | ||
|
||
it('should not find candidate with a course and already a reconciledAt', async function () { | ||
// given | ||
const userId = databaseBuilder.factory.buildUser().id; | ||
const sessionId = databaseBuilder.factory.buildSession({ userId }).id; | ||
databaseBuilder.factory.buildCertificationCourse({ userId, sessionId, createdAt: new Date() }); | ||
databaseBuilder.factory.buildCertificationCandidate({ | ||
userId, | ||
sessionId, | ||
reconciledAt: new Date(), | ||
}); | ||
await databaseBuilder.commit(); | ||
|
||
// when | ||
const results = await candidateRepository.findCandidateWithoutReconciledAt(); | ||
|
||
// then | ||
expect(results).to.be.empty; | ||
}); | ||
}); | ||
|
||
describe('update', function () { | ||
it('should update reconciledAt when modified', async function () { | ||
// given | ||
const oldReconciledDate = new Date('2024-01-01'); | ||
const newReconciledDate = new Date('2024-10-29'); | ||
const userId = databaseBuilder.factory.buildUser().id; | ||
const sessionId = databaseBuilder.factory.buildSession({ userId }).id; | ||
const candidateId = databaseBuilder.factory.buildCertificationCandidate({ | ||
userId, | ||
sessionId, | ||
reconciledAt: oldReconciledDate, | ||
}).id; | ||
await databaseBuilder.commit(); | ||
|
||
// when | ||
const rowChanged = await candidateRepository.update({ | ||
candidate: new Candidate({ id: candidateId, reconciledAt: newReconciledDate }), | ||
}); | ||
|
||
// then | ||
expect(rowChanged).to.equal(1); | ||
const reconciledAt = await knex('certification-candidates').pluck('reconciledAt').where({ id: candidateId }); | ||
expect(reconciledAt[0]).to.deep.equal(newReconciledDate); | ||
}); | ||
}); | ||
}); |
54 changes: 54 additions & 0 deletions
54
api/tests/integration/scripts/certification/finishing-reconciled-at-migration_test.js
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,54 @@ | ||
import { main } from '../../../../scripts/certification/finishing-reconciled-at-migration.js'; | ||
import { databaseBuilder, expect, knex } from '../../../test-helper.js'; | ||
|
||
describe('Integration | Scripts | Certification | finishing-reconciled-at-migration', function () { | ||
it('should fix the missing reconciledAt', async function () { | ||
// given | ||
const courseDate = new Date('2024-10-29'); | ||
const sessionId = databaseBuilder.factory.buildSession({}).id; | ||
|
||
const userIdOne = databaseBuilder.factory.buildUser().id; | ||
databaseBuilder.factory.buildCertificationCourse({ userId: userIdOne, sessionId, createdAt: courseDate }); | ||
const candidateIdOne = databaseBuilder.factory.buildCertificationCandidate({ | ||
userId: userIdOne, | ||
sessionId, | ||
}).id; | ||
|
||
const userIdTwo = databaseBuilder.factory.buildUser().id; | ||
databaseBuilder.factory.buildCertificationCourse({ userId: userIdTwo, sessionId, createdAt: courseDate }); | ||
const candidateIdTwo = databaseBuilder.factory.buildCertificationCandidate({ | ||
userId: userIdTwo, | ||
sessionId, | ||
}).id; | ||
|
||
const userIdThree = databaseBuilder.factory.buildUser().id; | ||
const candidateIdWithNoCourse = databaseBuilder.factory.buildCertificationCandidate({ | ||
userId: userIdThree, | ||
sessionId, | ||
}).id; | ||
await databaseBuilder.commit(); | ||
|
||
// Creating the issue we wanna fix in database | ||
await knex('certification-candidates') | ||
.update({ reconciledAt: null }) | ||
.whereIn('id', [candidateIdOne, candidateIdTwo, candidateIdWithNoCourse]); | ||
|
||
// when | ||
await main({ chunkSize: 1 }); | ||
|
||
// then | ||
const reconciledAts = await knex('certification-candidates') | ||
.pluck('reconciledAt') | ||
.whereIn('id', [candidateIdOne, candidateIdTwo]); | ||
|
||
expect(reconciledAts).to.have.lengthOf(2); | ||
for (const reconciledAt of reconciledAts) { | ||
expect(reconciledAt).to.deep.equal(courseDate); | ||
} | ||
|
||
const reconciledAtForNoCourse = await knex('certification-candidates') | ||
.pluck('reconciledAt') | ||
.where('id', candidateIdWithNoCourse); | ||
expect(reconciledAtForNoCourse[0]).to.be.null; | ||
}); | ||
}); |