-
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.
feat(api): add script to convert users orga cgu accepted to legal doc…
…ument user acceptances
- Loading branch information
Showing
2 changed files
with
291 additions
and
0 deletions.
There are no files selected for viewing
120 changes: 120 additions & 0 deletions
120
api/src/legal-documents/scripts/convert-users-orga-cgu-data.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,120 @@ | ||
import { setTimeout } from 'node:timers/promises'; | ||
|
||
import { DomainTransaction } from '../../../lib/infrastructure/DomainTransaction.js'; | ||
import { Script } from '../../shared/application/scripts/script.js'; | ||
import { ScriptRunner } from '../../shared/application/scripts/script-runner.js'; | ||
import { LegalDocumentService } from '../domain/models/LegalDocumentService.js'; | ||
import { LegalDocumentType } from '../domain/models/LegalDocumentType.js'; | ||
import * as legalDocumentRepository from '../infrastructure/repositories/legal-document.repository.js'; | ||
|
||
const { TOS } = LegalDocumentType.VALUES; | ||
const { PIX_ORGA } = LegalDocumentService.VALUES; | ||
|
||
const DEFAULT_BATCH_SIZE = 1000; | ||
const DEFAULT_THROTTLE_DELAY = 300; | ||
|
||
export class ConvertUsersOrgaCguData extends Script { | ||
constructor() { | ||
super({ | ||
description: 'Convert users orga CGU data to legal-document-versions-user-acceptances', | ||
permanent: false, | ||
options: { | ||
dryRun: { | ||
type: 'boolean', | ||
describe: 'Execute the script in dry run mode', | ||
default: false, | ||
}, | ||
batchSize: { | ||
type: 'number', | ||
describe: 'Size of the batch to process', | ||
default: DEFAULT_BATCH_SIZE, | ||
}, | ||
throttleDelay: { | ||
type: 'number', | ||
describe: 'Delay between batches in milliseconds', | ||
default: DEFAULT_THROTTLE_DELAY, | ||
}, | ||
}, | ||
}); | ||
} | ||
|
||
async handle({ logger, options }) { | ||
const { dryRun, batchSize, throttleDelay } = options; | ||
|
||
const legalDocument = await legalDocumentRepository.findLastVersionByTypeAndService({ | ||
type: TOS, | ||
service: PIX_ORGA, | ||
}); | ||
|
||
if (!legalDocument) { | ||
throw new Error(`No legal document found for type: ${TOS}, service: ${PIX_ORGA}`); | ||
} | ||
|
||
let hasMoreUsers = true; | ||
let offset = 0; | ||
let migrationCount = 0; | ||
|
||
while (hasMoreUsers) { | ||
const usersWithCgu = await this.#findOrgaCguAcceptedUsers({ batchSize, offset }); | ||
if (usersWithCgu.length === 0) { | ||
hasMoreUsers = false; | ||
break; | ||
} | ||
|
||
logger.info(`Batch #${Math.ceil(offset / batchSize + 1)}`); | ||
|
||
const usersToMigrate = await this.#filterAlreadyMigratedUsers({ legalDocument, users: usersWithCgu }); | ||
|
||
if (!dryRun) { | ||
await this.#createNewLegalDocumentAcceptanceForUsersBatch({ legalDocument, users: usersToMigrate }); | ||
} | ||
|
||
migrationCount += usersToMigrate.length; | ||
offset += usersWithCgu.length; | ||
|
||
await setTimeout(throttleDelay); | ||
} | ||
|
||
logger.info(`Total users ${dryRun ? 'to migrate' : 'migrated'}: ${migrationCount}`); | ||
} | ||
|
||
async #findOrgaCguAcceptedUsers({ batchSize, offset }) { | ||
const knexConnection = DomainTransaction.getConnection(); | ||
|
||
return knexConnection('users') | ||
.select('*') | ||
.where('pixOrgaTermsOfServiceAccepted', true) | ||
.limit(batchSize) | ||
.offset(offset); | ||
} | ||
|
||
async #filterAlreadyMigratedUsers({ legalDocument, users }) { | ||
const knexConnection = DomainTransaction.getConnection(); | ||
|
||
const alreadyMigratedUsers = await knexConnection('legal-document-version-user-acceptances') | ||
.select('userId') | ||
.where('legalDocumentVersionId', legalDocument.id) | ||
.whereIn( | ||
'userId', | ||
users.map((user) => user.id), | ||
); | ||
|
||
const alreadyMigratedIds = alreadyMigratedUsers.map((user) => user.userId); | ||
return users.filter((user) => !alreadyMigratedIds.includes(user.id)); | ||
} | ||
|
||
async #createNewLegalDocumentAcceptanceForUsersBatch({ legalDocument, users }) { | ||
const knexConnection = DomainTransaction.getConnection(); | ||
|
||
const chunkSize = 100; | ||
const rows = users.map((user) => ({ | ||
userId: user.id, | ||
legalDocumentVersionId: legalDocument.id, | ||
acceptedAt: user.lastPixOrgaTermsOfServiceValidatedAt || new Date(), | ||
})); | ||
|
||
await knexConnection.batchInsert('legal-document-version-user-acceptances', rows, chunkSize); | ||
} | ||
} | ||
|
||
await ScriptRunner.execute(import.meta.url, ConvertUsersOrgaCguData); |
171 changes: 171 additions & 0 deletions
171
api/tests/legal-documents/integration/scripts/convert-users-orga-cgu-data.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,171 @@ | ||
import { LegalDocumentService } from '../../../../src/legal-documents/domain/models/LegalDocumentService.js'; | ||
import { LegalDocumentType } from '../../../../src/legal-documents/domain/models/LegalDocumentType.js'; | ||
import { ConvertUsersOrgaCguData } from '../../../../src/legal-documents/scripts/convert-users-orga-cgu-data.js'; | ||
import { databaseBuilder, expect, knex, sinon } from '../../../test-helper.js'; | ||
|
||
const { TOS } = LegalDocumentType.VALUES; | ||
const { PIX_ORGA } = LegalDocumentService.VALUES; | ||
|
||
describe('Integration | Legal documents | Scripts | convert-users-orga-cgu-data', function () { | ||
describe('Options', function () { | ||
it('has the correct options', function () { | ||
// given | ||
const script = new ConvertUsersOrgaCguData(); | ||
|
||
// when | ||
const { options, permanent } = script.metaInfo; | ||
|
||
// then | ||
expect(permanent).to.be.false; | ||
expect(options).to.deep.include({ | ||
dryRun: { | ||
type: 'boolean', | ||
describe: 'Execute the script in dry run mode', | ||
default: false, | ||
}, | ||
batchSize: { | ||
type: 'number', | ||
describe: 'Size of the batch to process', | ||
default: 1000, | ||
}, | ||
throttleDelay: { | ||
type: 'number', | ||
describe: 'Delay between batches in milliseconds', | ||
default: 300, | ||
}, | ||
}); | ||
}); | ||
}); | ||
|
||
describe('#handle', function () { | ||
let clock; | ||
let legalDocumentVersion; | ||
let logger; | ||
|
||
beforeEach(async function () { | ||
clock = sinon.useFakeTimers({ now: new Date('2024-01-01'), toFake: ['Date'] }); | ||
legalDocumentVersion = databaseBuilder.factory.buildLegalDocumentVersion({ type: TOS, service: PIX_ORGA }); | ||
logger = { info: sinon.stub() }; | ||
}); | ||
|
||
afterEach(async function () { | ||
clock.restore(); | ||
}); | ||
|
||
it('converts Pix Orga user cgus to legal documents acceptances', async function () { | ||
// given | ||
const userAcceptedCgu = databaseBuilder.factory.buildUser({ | ||
pixOrgaTermsOfServiceAccepted: true, | ||
lastPixOrgaTermsOfServiceValidatedAt: new Date('2021-01-01'), | ||
}); | ||
const userAcceptedCguWithoutDate = databaseBuilder.factory.buildUser({ | ||
pixOrgaTermsOfServiceAccepted: true, | ||
lastPixOrgaTermsOfServiceValidatedAt: null, | ||
}); | ||
const userNotAcceptedCgu = databaseBuilder.factory.buildUser({ | ||
pixOrgaTermsOfServiceAccepted: false, | ||
lastPixOrgaTermsOfServiceValidatedAt: null, | ||
}); | ||
await databaseBuilder.commit(); | ||
|
||
// when | ||
const script = new ConvertUsersOrgaCguData(); | ||
const options = { dryRun: false, batchSize: 1, throttleDelay: 0 }; | ||
await script.handle({ options, logger }); | ||
|
||
// then | ||
expect(logger.info).to.have.been.calledWith('Batch #1'); | ||
expect(logger.info).to.have.been.calledWith('Batch #2'); | ||
expect(logger.info).to.have.been.calledWith('Total users migrated: 2'); | ||
|
||
const userAcceptances = await knex('legal-document-version-user-acceptances').where({ | ||
legalDocumentVersionId: legalDocumentVersion.id, | ||
}); | ||
|
||
const acceptance1 = userAcceptances.find((user) => user.userId === userAcceptedCgu.id); | ||
expect(acceptance1.acceptedAt).to.deep.equal(new Date('2021-01-01')); | ||
|
||
const acceptance2 = userAcceptances.find((user) => user.userId === userAcceptedCguWithoutDate.id); | ||
expect(acceptance2.acceptedAt).to.deep.equal(new Date('2024-01-01')); | ||
|
||
const acceptance3 = userAcceptances.find((user) => user.userId === userNotAcceptedCgu.id); | ||
expect(acceptance3).to.be.undefined; | ||
}); | ||
|
||
context('when a user cgu acceptance is already converted', function () { | ||
it('does not change already converted users', async function () { | ||
// given | ||
const alreadyConvertedUser = databaseBuilder.factory.buildUser({ | ||
pixOrgaTermsOfServiceAccepted: true, | ||
lastPixOrgaTermsOfServiceValidatedAt: new Date('2021-01-01'), | ||
}); | ||
const newUserAcceptedCgu = databaseBuilder.factory.buildUser({ | ||
pixOrgaTermsOfServiceAccepted: true, | ||
lastPixOrgaTermsOfServiceValidatedAt: new Date('2021-01-01'), | ||
}); | ||
databaseBuilder.factory.buildLegalDocumentVersionUserAcceptance({ | ||
legalDocumentVersionId: legalDocumentVersion.id, | ||
userId: alreadyConvertedUser.id, | ||
acceptedAt: new Date('2020-01-01'), | ||
}); | ||
await databaseBuilder.commit(); | ||
|
||
// when | ||
const script = new ConvertUsersOrgaCguData(); | ||
const options = { dryRun: false, batchSize: 1, throttleDelay: 0 }; | ||
await script.handle({ options, logger }); | ||
|
||
// then | ||
expect(logger.info).to.have.been.calledWith('Total users migrated: 1'); | ||
|
||
const userAcceptances = await knex('legal-document-version-user-acceptances').where({ | ||
legalDocumentVersionId: legalDocumentVersion.id, | ||
}); | ||
|
||
const acceptance1 = userAcceptances.find((user) => user.userId === alreadyConvertedUser.id); | ||
expect(acceptance1.acceptedAt).to.deep.equal(new Date('2020-01-01')); | ||
|
||
const acceptance2 = userAcceptances.find((user) => user.userId === newUserAcceptedCgu.id); | ||
expect(acceptance2.acceptedAt).to.deep.equal(new Date('2021-01-01')); | ||
}); | ||
}); | ||
|
||
context('when dry run mode', function () { | ||
it('does not create legal document user acceptance', async function () { | ||
// given | ||
databaseBuilder.factory.buildUser({ | ||
pixOrgaTermsOfServiceAccepted: true, | ||
lastPixOrgaTermsOfServiceValidatedAt: new Date('2021-01-01'), | ||
}); | ||
await databaseBuilder.commit(); | ||
|
||
// when | ||
const script = new ConvertUsersOrgaCguData(); | ||
const options = { dryRun: true, batchSize: 1, throttleDelay: 0 }; | ||
await script.handle({ options, logger }); | ||
|
||
// then | ||
expect(logger.info).to.have.been.calledWith('Batch #1'); | ||
expect(logger.info).to.have.been.calledWith('Total users to migrate: 1'); | ||
|
||
const userAcceptances = await knex('legal-document-version-user-acceptances').where({ | ||
legalDocumentVersionId: legalDocumentVersion.id, | ||
}); | ||
expect(userAcceptances.length).to.equal(0); | ||
}); | ||
}); | ||
|
||
context('when no legal document is found', function () { | ||
it('throws an error', async function () { | ||
// given | ||
const script = new ConvertUsersOrgaCguData(); | ||
|
||
// when / then | ||
const options = { dryRun: true, batchSize: 1, throttleDelay: 0 }; | ||
await expect(script.handle({ options, logger })).to.be.rejectedWith( | ||
'No legal document found for type: TOS, service: pix-orga', | ||
); | ||
}); | ||
}); | ||
}); | ||
}); |