Skip to content

Commit

Permalink
feat(api): add script to convert users orga cgu accepted to legal doc…
Browse files Browse the repository at this point in the history
…ument user acceptances
  • Loading branch information
P-Jeremy authored and bpetetot committed Dec 16, 2024
1 parent f8ea8e3 commit 9cf1281
Show file tree
Hide file tree
Showing 2 changed files with 291 additions and 0 deletions.
120 changes: 120 additions & 0 deletions api/src/legal-documents/scripts/convert-users-orga-cgu-data.js
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);
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',
);
});
});
});
});

0 comments on commit 9cf1281

Please sign in to comment.