Skip to content

Commit

Permalink
feat(api): add sixth grade attestation script
Browse files Browse the repository at this point in the history
  • Loading branch information
Guillaume committed Dec 17, 2024
1 parent 70c5490 commit d682922
Show file tree
Hide file tree
Showing 2 changed files with 364 additions and 0 deletions.
112 changes: 112 additions & 0 deletions api/src/profile/scripts/sixth-grade-attestation-reward.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import dayjs from 'dayjs';

import { CampaignParticipationStatuses } from '../../prescription/shared/domain/constants.js';
import { usecases } from '../../quest/domain/usecases/index.js';
import { isoDateParser } from '../../shared/application/scripts/parsers.js';
import { Script } from '../../shared/application/scripts/script.js';
import { ScriptRunner } from '../../shared/application/scripts/script-runner.js';
import { DomainTransaction } from '../../shared/domain/DomainTransaction.js';

export const PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS = [1000000, 1000001, 1000002];

const options = {
start: {
type: 'string',
describe: 'Date de début de la période à traiter, jour inclus, format "YYYY-MM-DD", (ex: "2024-01-20")',
demandOption: false,
requiresArg: true,
coerce: isoDateParser(),
},
end: {
type: 'string',
describe: 'Date de fin de la période à traiter, jour inclus, format "YYYY-MM-DD", (ex: "2024-02-27")',
demandOption: true,
requiresArg: true,
coerce: isoDateParser(),
},
};

/**
* Script to reward sixth-grade students who have already completed a campaign linked to a specific target profile.
*/
export class SixthGradeAttestationRewardScript extends Script {
constructor() {
super({
description:
'This script process attestations rewards for users who have already completed a campaign linked to specific target profiles.',
permanent: false,
options,
});
}

/**
* Handles the core logic of the script.
*
* @param {{start: Date, end: Date, limit: number}} options
* @param {{info: function}} logger
* @param {function} rewardUser
*
* @returns {Promise<void>}
*/
async handle({ options, logger, rewardUser = usecases.rewardUser }) {
this.checkEndDateBeforeStartDate(options.start, options.end);

const users = await this.fetchUserIds(options.start, options.end);

if (users.length === 0) {
logger.info('No user found');
return;
}

logger.info(`${users.length} users found`);

for (const userId of users) {
logger.info(`Processing user ${userId}`);
await rewardUser({
userId,
});
}
}

/**
* Fetch the userIDs of the users who have already completed a campaign linked to the specified target profiles.
*
* @param {Date} startDate
* @param {Date} endDate
*
* @returns {Promise<[number]>}
*/
async fetchUserIds(startDate, endDate) {
const knexConnection = DomainTransaction.getConnection();

const formatedStartDate = startDate.toISOString().split('T')[0];

endDate.setDate(endDate.getDate() + 1);
const formatedEndDate = endDate.toISOString().split('T')[0];

const users = await knexConnection('campaign-participations')
.select('campaign-participations.userId')
.join('campaigns', 'campaign-participations.campaignId', 'campaigns.id')
.join('target-profiles', 'campaigns.targetProfileId', 'target-profiles.id')
.where('campaign-participations.createdAt', '>=', formatedStartDate)
.where('campaign-participations.createdAt', '<=', formatedEndDate)
.where('campaign-participations.status', '<>', CampaignParticipationStatuses.STARTED)
.whereIn('campaigns.targetProfileId', PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS);

return users.map(({ userId }) => userId);
}

/**
* Check if the end date is before the start date.
*
* @param {Date} startDate
* @param {Date} endDate
*/
checkEndDateBeforeStartDate(startDate, endDate) {
if (endDate < startDate) {
throw new Error('The end date must be greater than the start date');
}
}
}

await ScriptRunner.execute(import.meta.url, SixthGradeAttestationRewardScript);
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import sinon from 'sinon';

import { CampaignParticipationStatuses } from '../../../../src/prescription/shared/domain/constants.js';
import {
PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS,
SixthGradeAttestationRewardScript,
} from '../../../../src/profile/scripts/sixth-grade-attestation-reward.js';
import { catchErr, databaseBuilder, expect } from '../../../test-helper.js';

describe('Integration | Profile | Scripts | sixth-grade-attestation-reward', function () {
describe('options', function () {
it('parses dates correctly', function () {
const startDate = '2024-01-01';
const endDate = { whut: 'idontknow' };
const script = new SixthGradeAttestationRewardScript();
const { options } = script.metaInfo;
const parsedDate = options.start.coerce(startDate);
expect(parsedDate).to.be.a.instanceOf(Date);
expect(() => options.end.coerce(endDate)).to.throw();
});
});

describe('#fetchUsers', function () {
let script;

beforeEach(async function () {
script = new SixthGradeAttestationRewardScript();
});

it('should not return the user if the participation date is not included between the start date and the end date ', async function () {
const { id: targetProfileId } = databaseBuilder.factory.buildTargetProfile({
id: PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS[0],
});
const campaign = databaseBuilder.factory.buildCampaign({ targetProfileId });
const { userId } = databaseBuilder.factory.buildCampaignParticipation({
campaignId: campaign.id,
status: CampaignParticipationStatuses.SHARED,
createdAt: '2024-12-02',
});
databaseBuilder.factory.buildCampaignParticipation({
campaignId: campaign.id,
status: CampaignParticipationStatuses.SHARED,
createdAt: '2024-12-07',
});
databaseBuilder.factory.buildCampaignParticipation({
campaignId: campaign.id,
status: CampaignParticipationStatuses.SHARED,
createdAt: '2024-11-22',
});

await databaseBuilder.commit();
const userIds = await script.fetchUserIds(new Date('2024-12-01'), new Date('2024-12-03'));
expect(userIds).to.have.lengthOf(1);
expect(userIds).to.contains(userId);
});

it('should only return the user if participation status is different than started', async function () {
const { id: targetProfileId } = databaseBuilder.factory.buildTargetProfile({
id: PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS[0],
});
const campaign = databaseBuilder.factory.buildCampaign({ targetProfileId });
const { userId } = databaseBuilder.factory.buildCampaignParticipation({
campaignId: campaign.id,
status: CampaignParticipationStatuses.STARTED,
createdAt: '2024-12-02',
});
databaseBuilder.factory.buildCampaignParticipation({
campaignId: campaign.id,
status: CampaignParticipationStatuses.SHARED,
createdAt: '2024-12-02',
});
databaseBuilder.factory.buildCampaignParticipation({
campaignId: campaign.id,
status: CampaignParticipationStatuses.TO_SHARE,
createdAt: '2024-12-02',
});

await databaseBuilder.commit();
const userIds = await script.fetchUserIds(new Date('2024-12-01'), new Date('2024-12-06'));
expect(userIds).to.have.lengthOf(2);
expect(userIds).to.not.contains(userId);
});

it('should not return the user if the campaign target profile is not included in targeted target profiles', async function () {
const { id: targetProfileId1 } = databaseBuilder.factory.buildTargetProfile({
id: PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS[0],
});
const { id: targetProfileId2 } = databaseBuilder.factory.buildTargetProfile({
id: PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS[1],
});
const { id: targetProfileId3 } = databaseBuilder.factory.buildTargetProfile({
id: PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS[2],
});
const { id: targetProfileId4 } = databaseBuilder.factory.buildTargetProfile();
const campaign1 = databaseBuilder.factory.buildCampaign({ targetProfileId: targetProfileId1 });
const campaign2 = databaseBuilder.factory.buildCampaign({ targetProfileId: targetProfileId2 });
const campaign3 = databaseBuilder.factory.buildCampaign({ targetProfileId: targetProfileId3 });
const campaign4 = databaseBuilder.factory.buildCampaign({ targetProfileId: targetProfileId4 });
const otherParameters = { status: CampaignParticipationStatuses.SHARED, createdAt: '2024-12-02T15:07:57.376Z' };
databaseBuilder.factory.buildCampaignParticipation({
campaignId: campaign1.id,
...otherParameters,
});
databaseBuilder.factory.buildCampaignParticipation({
campaignId: campaign2.id,
...otherParameters,
});
databaseBuilder.factory.buildCampaignParticipation({
campaignId: campaign3.id,
...otherParameters,
});
const { userId: otherTargetProfileCampaignParticipationUserId } =
databaseBuilder.factory.buildCampaignParticipation({
campaignId: campaign4.id,
...otherParameters,
});

await databaseBuilder.commit();
const userIds = await script.fetchUserIds(new Date('2024-12-01'), new Date('2024-12-06'));
expect(userIds).to.have.lengthOf(3);
expect(userIds).to.not.contains(otherTargetProfileCampaignParticipationUserId);
});

it('should return expected users', async function () {
const { id: targetProfileId1 } = databaseBuilder.factory.buildTargetProfile({
id: PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS[0],
});
const { id: targetProfileId2 } = databaseBuilder.factory.buildTargetProfile({
id: PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS[1],
});
const { id: targetProfileId3 } = databaseBuilder.factory.buildTargetProfile({
id: PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS[2],
});
const campaign1 = databaseBuilder.factory.buildCampaign({ targetProfileId: targetProfileId1 });
const campaign2 = databaseBuilder.factory.buildCampaign({ targetProfileId: targetProfileId2 });
const campaign3 = databaseBuilder.factory.buildCampaign({ targetProfileId: targetProfileId3 });
databaseBuilder.factory.buildCampaignParticipation({
campaignId: campaign1.id,
status: CampaignParticipationStatuses.SHARED,
createdAt: '2024-12-01T00:00:01.376Z',
});
databaseBuilder.factory.buildCampaignParticipation({
campaignId: campaign2.id,
status: CampaignParticipationStatuses.TO_SHARE,
createdAt: '2024-12-07T15:07:57.376Z',
});
databaseBuilder.factory.buildCampaignParticipation({
campaignId: campaign3.id,
status: CampaignParticipationStatuses.TO_SHARE,
createdAt: '2024-12-07T23:59:59.376Z',
});
databaseBuilder.factory.buildCampaignParticipation({
campaignId: campaign3.id,
status: CampaignParticipationStatuses.TO_SHARE,
createdAt: '2024-12-31T21:22:00.001Z',
});
databaseBuilder.factory.buildCampaignParticipation({
campaignId: campaign3.id,
status: CampaignParticipationStatuses.TO_SHARE,
createdAt: '2025-01-01T21:22:00.001Z',
});

await databaseBuilder.commit();
const userIds = await script.fetchUserIds(new Date('2024-12-01'), new Date('2024-12-31'));
expect(userIds).to.have.lengthOf(4);
});
});

describe('#handle', function () {
it('should log information for each userId', async function () {
const { id: targetProfileId1 } = databaseBuilder.factory.buildTargetProfile({
id: PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS[2],
});
const { id: targetProfileId2 } = databaseBuilder.factory.buildTargetProfile({
id: PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS[0],
});
const { id: targetProfileId3 } = databaseBuilder.factory.buildTargetProfile({
id: PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS[1],
});
const campaign1 = databaseBuilder.factory.buildCampaign({ targetProfileId: targetProfileId1 });
const campaign2 = databaseBuilder.factory.buildCampaign({ targetProfileId: targetProfileId2 });
const campaign3 = databaseBuilder.factory.buildCampaign({ targetProfileId: targetProfileId3 });
databaseBuilder.factory.buildCampaignParticipation({
campaignId: campaign1.id,
status: CampaignParticipationStatuses.SHARED,
createdAt: '2024-12-01',
});
databaseBuilder.factory.buildCampaignParticipation({
campaignId: campaign2.id,
status: CampaignParticipationStatuses.TO_SHARE,
createdAt: '2024-12-02',
});
databaseBuilder.factory.buildCampaignParticipation({
campaignId: campaign3.id,
status: CampaignParticipationStatuses.TO_SHARE,
createdAt: '2024-12-03',
});

await databaseBuilder.commit();

const script = new SixthGradeAttestationRewardScript();
const logger = { info: sinon.spy(), error: sinon.spy() };
const usecases = { rewardUser: sinon.stub() };

await script.handle({
options: {
start: new Date('2024-12-01'),
end: new Date('2024-12-07'),
},
logger,
rewardUser: usecases.rewardUser,
});

expect(logger.info.callCount).to.equal(4);
});

it('should throw an error if end comes before start.', async function () {
const script = new SixthGradeAttestationRewardScript();
const logger = { info: sinon.spy(), error: sinon.spy() };
const usecases = { rewardUser: sinon.stub() };

const error = await catchErr(script.handle)({
options: {
start: new Date('2024-11-10'),
end: new Date('2024-11-09'),
},
logger,
rewardUser: usecases.rewardUser,
});

expect(error).to.be.an.instanceOf(Error);
});

it('should stop execution if there are no users', async function () {
const script = new SixthGradeAttestationRewardScript();
const logger = { info: sinon.spy(), error: sinon.spy() };
const usecases = { rewardUser: sinon.stub() };

await script.handle({
options: {
start: new Date('2024-12-01'),
end: new Date('2024-12-09'),
},
logger,
rewardUser: usecases.rewardUser,
});

expect(usecases.rewardUser).to.not.have.been.called;
expect(logger.info).to.have.been.calledOnceWithExactly('No user found');
});
});
});

0 comments on commit d682922

Please sign in to comment.