Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { commaSeparatedNumberParser } from '../../shared/application/scripts/parsers.js';
import { Script } from '../../shared/application/scripts/script.js';
import { ScriptRunner } from '../../shared/application/scripts/script-runner.js';
import { CAMPAIGN_FEATURES } from '../../shared/constants.js';
import { DomainTransaction } from '../../shared/domain/DomainTransaction.js';

export class SetHighlightedTrainingsForCampaignScript extends Script {
constructor() {
super({
description:
'Sets the highlighted training ids (params.highlightedTrainingIds) for a campaign already having the RECOMMENDATION_ENGINE feature enabled',
permanent: false,
options: {
campaignId: {
type: 'number',
describe: 'the campaign id',
demandOption: true,
},
highlightedTrainingIds: {
type: 'string',
describe: 'a list of comma separated training ids to highlight',
demandOption: true,
coerce: commaSeparatedNumberParser(),
},
dryRun: {
type: 'boolean',
describe: 'Run the script without making any database changes',
default: false,

@matthiasferraina matthiasferraina Sep 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: le mettre à true par defaut

},
},
});
}

async handle({ options, logger }) {
await DomainTransaction.execute(async () => {
const knexConn = DomainTransaction.getConnection();
const { campaignId, highlightedTrainingIds } = options;

const feature = await knexConn('features').where({ key: CAMPAIGN_FEATURES.RECOMMENDATION_ENGINE.key }).first();
if (!feature) {
throw new Error(`Feature ${CAMPAIGN_FEATURES.RECOMMENDATION_ENGINE.key} not found in "features" table`);
}

const campaignFeature = await knexConn('campaign-features').where({ campaignId, featureId: feature.id }).first();
if (!campaignFeature) {
throw new Error(
`Campaign ${campaignId} does not have the ${CAMPAIGN_FEATURES.RECOMMENDATION_ENGINE.key} feature enabled`,
);
}

const foundTrainings = await knexConn('trainings').whereIn('id', highlightedTrainingIds);
const foundTrainingIds = foundTrainings.map(({ id }) => id);
const missingTrainingIds = highlightedTrainingIds.filter((trainingId) => !foundTrainingIds.includes(trainingId));
if (missingTrainingIds.length > 0) {
throw new Error(`Training(s) not found in "trainings" table: ${missingTrainingIds.join(', ')}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: c'est la liste des CF mis en avant manquant, ou des CF en général manquant ?

}

await knexConn('campaign-features')
.where({ campaignId, featureId: feature.id })
.update({ params: { highlightedTrainingIds } });

logger.info(`Campaign ${campaignId}: highlightedTrainingIds set to [${highlightedTrainingIds.join(', ')}]`);

if (options.dryRun) {
await knexConn.rollback();
logger.info('ROLLBACK: no changes were persisted (dry run)');
return;
}

logger.info('COMMIT: changes persisted');
});
}
}

await ScriptRunner.execute(import.meta.url, SetHighlightedTrainingsForCampaignScript);
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { expect } from 'chai';
import sinon from 'sinon';

import { SetHighlightedTrainingsForCampaignScript } from '../../../../src/prescription/scripts/set-highlighted-trainings-for-campaign.js';
import { CAMPAIGN_FEATURES } from '../../../../src/shared/constants.js';
import { databaseBuilder, knex } from '../../../tooling/databases.js';
import { catchErr } from '../../../tooling/test-utils/error.js';

describe('SetHighlightedTrainingsForCampaignScript', function () {
describe('Options', function () {
it('has the correct options', function () {
// given & when
const script = new SetHighlightedTrainingsForCampaignScript();
const { options } = script.metaInfo;

// then
expect(options.campaignId).to.deep.include({
type: 'number',
describe: 'the campaign id',
demandOption: true,
});

expect(options.highlightedTrainingIds).to.deep.include({
type: 'string',
describe: 'a list of comma separated training ids to highlight',
demandOption: true,
});

expect(options.dryRun).to.deep.include({
type: 'boolean',
describe: 'Run the script without making any database changes',
default: false,
});
});

it('parses list of highlightedTrainingIds', async function () {
// given & when
const ids = '1,2,3';
const script = new SetHighlightedTrainingsForCampaignScript();
const { options } = script.metaInfo;
const parsedData = await options.highlightedTrainingIds.coerce(ids);

// then
expect(parsedData).to.deep.equals([1, 2, 3]);
});
});

describe('Handle', function () {
let script;
let logger;
let featureId;

beforeEach(async function () {
script = new SetHighlightedTrainingsForCampaignScript();
logger = { info: sinon.spy(), error: sinon.spy() };
featureId = databaseBuilder.factory.buildFeature(CAMPAIGN_FEATURES.RECOMMENDATION_ENGINE).id;
await databaseBuilder.commit();
});

context('when the campaign does not have the RECOMMENDATION_ENGINE feature enabled', function () {
it('throws an error and does not update anything', async function () {
// given
const campaign = databaseBuilder.factory.buildCampaign();
await databaseBuilder.commit();

// when
const error = await catchErr(script.handle)({
options: { campaignId: campaign.id, highlightedTrainingIds: [1], dryRun: false },
logger,
});

// then
expect(error.message).to.equal(
`Campaign ${campaign.id} does not have the ${CAMPAIGN_FEATURES.RECOMMENDATION_ENGINE.key} feature enabled`,
);
});
});

context('when a highlighted training id does not exist', function () {
it('throws an error and does not update the campaign feature', async function () {
// given
const campaign = databaseBuilder.factory.buildCampaign();
databaseBuilder.factory.buildCampaignFeature({ campaignId: campaign.id, featureId, params: {} });
const training = databaseBuilder.factory.buildTraining();
await databaseBuilder.commit();

const missingTrainingId = training.id + 1000;

// when
const error = await catchErr(script.handle)({
options: {
campaignId: campaign.id,
highlightedTrainingIds: [training.id, missingTrainingId],
dryRun: false,
},
logger,
});

// then
expect(error.message).to.equal(`Training(s) not found in "trainings" table: ${missingTrainingId}`);

const campaignFeature = await knex('campaign-features').where({ campaignId: campaign.id, featureId }).first();
expect(campaignFeature.params).to.deep.equal({});
});
});

context('when the campaign has the feature enabled and all training ids exist', function () {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
context('when the campaign has the feature enabled and all training ids exist', function () {
context('when the campaign has the recommandation engine feature enabled, and all provided training ids exist', function () {

it('overwrites params with the highlighted training ids', async function () {
// given
const campaign = databaseBuilder.factory.buildCampaign();
databaseBuilder.factory.buildCampaignFeature({
campaignId: campaign.id,
featureId,
params: { someOtherKey: 'shouldBeOverwritten' },
});
const training1 = databaseBuilder.factory.buildTraining();
const training2 = databaseBuilder.factory.buildTraining();
await databaseBuilder.commit();

// when
await script.handle({
options: { campaignId: campaign.id, highlightedTrainingIds: [training1.id, training2.id], dryRun: false },
logger,
});

// then
const campaignFeature = await knex('campaign-features').where({ campaignId: campaign.id, featureId }).first();
expect(campaignFeature.params).to.deep.equal({ highlightedTrainingIds: [training1.id, training2.id] });
expect(logger.info.calledWithMatch('COMMIT: changes persisted')).to.be.true;
});
});

context('when dryRun is present', function () {
it('does not persist any database changes', async function () {
// given
const campaign = databaseBuilder.factory.buildCampaign();
databaseBuilder.factory.buildCampaignFeature({ campaignId: campaign.id, featureId, params: {} });
const training = databaseBuilder.factory.buildTraining();
await databaseBuilder.commit();

// when
await script.handle({
options: { campaignId: campaign.id, highlightedTrainingIds: [training.id], dryRun: true },
logger,
});

// then
const campaignFeature = await knex('campaign-features').where({ campaignId: campaign.id, featureId }).first();
expect(campaignFeature.params).to.deep.equal({});
expect(logger.info.calledWithMatch('ROLLBACK')).to.be.true;
});
});
});
});
Loading