Skip to content

Commit 3592a18

Browse files
author
Guillaume
committed
feat(api): add sixth grade attestation script
1 parent 70c5490 commit 3592a18

File tree

2 files changed

+362
-0
lines changed

2 files changed

+362
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { CampaignParticipationStatuses } from '../../prescription/shared/domain/constants.js';
2+
import { usecases } from '../../quest/domain/usecases/index.js';
3+
import { isoDateParser } from '../../shared/application/scripts/parsers.js';
4+
import { Script } from '../../shared/application/scripts/script.js';
5+
import { ScriptRunner } from '../../shared/application/scripts/script-runner.js';
6+
import { DomainTransaction } from '../../shared/domain/DomainTransaction.js';
7+
8+
export const PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS = [1000000, 1000001, 1000002];
9+
10+
const options = {
11+
start: {
12+
type: 'string',
13+
describe: 'Date de début de la période à traiter, jour inclus, format "YYYY-MM-DD", (ex: "2024-01-20")',
14+
demandOption: false,
15+
requiresArg: true,
16+
coerce: isoDateParser(),
17+
},
18+
end: {
19+
type: 'string',
20+
describe: 'Date de fin de la période à traiter, jour inclus, format "YYYY-MM-DD", (ex: "2024-02-27")',
21+
demandOption: true,
22+
requiresArg: true,
23+
coerce: isoDateParser(),
24+
},
25+
};
26+
27+
/**
28+
* Script to reward sixth-grade students who have already completed a campaign linked to a specific target profile.
29+
*/
30+
export class SixthGradeAttestationRewardScript extends Script {
31+
constructor() {
32+
super({
33+
description:
34+
'This script process attestations rewards for users who have already completed a campaign linked to specific target profiles.',
35+
permanent: false,
36+
options,
37+
});
38+
}
39+
40+
/**
41+
* Handles the core logic of the script.
42+
*
43+
* @param {{start: Date, end: Date, limit: number}} options
44+
* @param {{info: function}} logger
45+
* @param {function} rewardUser
46+
*
47+
* @returns {Promise<void>}
48+
*/
49+
async handle({ options, logger, rewardUser = usecases.rewardUser }) {
50+
this.checkEndDateBeforeStartDate(options.start, options.end);
51+
52+
const users = await this.fetchUserIds(options.start, options.end);
53+
54+
if (users.length === 0) {
55+
logger.info('No user found');
56+
return;
57+
}
58+
59+
logger.info(`${users.length} users found`);
60+
61+
for (const userId of users) {
62+
logger.info(`Processing user ${userId}`);
63+
await rewardUser({
64+
userId,
65+
});
66+
}
67+
}
68+
69+
/**
70+
* Fetch the userIDs of the users who have already completed a campaign linked to the specified target profiles.
71+
*
72+
* @param {Date} startDate
73+
* @param {Date} endDate
74+
*
75+
* @returns {Promise<[number]>}
76+
*/
77+
async fetchUserIds(startDate, endDate) {
78+
const knexConnection = DomainTransaction.getConnection();
79+
80+
const formatedStartDate = startDate.toISOString().split('T')[0];
81+
82+
endDate.setDate(endDate.getDate() + 1);
83+
const formatedEndDate = endDate.toISOString().split('T')[0];
84+
85+
const users = await knexConnection('campaign-participations')
86+
.select('campaign-participations.userId')
87+
.join('campaigns', 'campaign-participations.campaignId', 'campaigns.id')
88+
.join('target-profiles', 'campaigns.targetProfileId', 'target-profiles.id')
89+
.where('campaign-participations.createdAt', '>=', formatedStartDate)
90+
.where('campaign-participations.createdAt', '<=', formatedEndDate)
91+
.where('campaign-participations.status', '<>', CampaignParticipationStatuses.STARTED)
92+
.whereIn('campaigns.targetProfileId', PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS);
93+
94+
return users.map(({ userId }) => userId);
95+
}
96+
97+
/**
98+
* Check if the end date is before the start date.
99+
*
100+
* @param {Date} startDate
101+
* @param {Date} endDate
102+
*/
103+
checkEndDateBeforeStartDate(startDate, endDate) {
104+
if (endDate < startDate) {
105+
throw new Error('The end date must be greater than the start date');
106+
}
107+
}
108+
}
109+
110+
await ScriptRunner.execute(import.meta.url, SixthGradeAttestationRewardScript);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import sinon from 'sinon';
2+
3+
import { CampaignParticipationStatuses } from '../../../../src/prescription/shared/domain/constants.js';
4+
import {
5+
PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS,
6+
SixthGradeAttestationRewardScript,
7+
} from '../../../../src/profile/scripts/sixth-grade-attestation-reward.js';
8+
import { catchErr, databaseBuilder, expect } from '../../../test-helper.js';
9+
10+
describe('Integration | Profile | Scripts | sixth-grade-attestation-reward', function () {
11+
describe('options', function () {
12+
it('parses dates correctly', function () {
13+
const startDate = '2024-01-01';
14+
const endDate = { whut: 'idontknow' };
15+
const script = new SixthGradeAttestationRewardScript();
16+
const { options } = script.metaInfo;
17+
const parsedDate = options.start.coerce(startDate);
18+
expect(parsedDate).to.be.a.instanceOf(Date);
19+
expect(() => options.end.coerce(endDate)).to.throw();
20+
});
21+
});
22+
23+
describe('#fetchUsers', function () {
24+
let script;
25+
26+
beforeEach(async function () {
27+
script = new SixthGradeAttestationRewardScript();
28+
});
29+
30+
it('should not return the user if the participation date is not included between the start date and the end date ', async function () {
31+
const { id: targetProfileId } = databaseBuilder.factory.buildTargetProfile({
32+
id: PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS[0],
33+
});
34+
const campaign = databaseBuilder.factory.buildCampaign({ targetProfileId });
35+
const { userId } = databaseBuilder.factory.buildCampaignParticipation({
36+
campaignId: campaign.id,
37+
status: CampaignParticipationStatuses.SHARED,
38+
createdAt: '2024-12-02',
39+
});
40+
databaseBuilder.factory.buildCampaignParticipation({
41+
campaignId: campaign.id,
42+
status: CampaignParticipationStatuses.SHARED,
43+
createdAt: '2024-12-07',
44+
});
45+
databaseBuilder.factory.buildCampaignParticipation({
46+
campaignId: campaign.id,
47+
status: CampaignParticipationStatuses.SHARED,
48+
createdAt: '2024-11-22',
49+
});
50+
51+
await databaseBuilder.commit();
52+
const userIds = await script.fetchUserIds(new Date('2024-12-01'), new Date('2024-12-03'));
53+
expect(userIds).to.have.lengthOf(1);
54+
expect(userIds).to.contains(userId);
55+
});
56+
57+
it('should only return the user if participation status is different than started', async function () {
58+
const { id: targetProfileId } = databaseBuilder.factory.buildTargetProfile({
59+
id: PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS[0],
60+
});
61+
const campaign = databaseBuilder.factory.buildCampaign({ targetProfileId });
62+
const { userId } = databaseBuilder.factory.buildCampaignParticipation({
63+
campaignId: campaign.id,
64+
status: CampaignParticipationStatuses.STARTED,
65+
createdAt: '2024-12-02',
66+
});
67+
databaseBuilder.factory.buildCampaignParticipation({
68+
campaignId: campaign.id,
69+
status: CampaignParticipationStatuses.SHARED,
70+
createdAt: '2024-12-02',
71+
});
72+
databaseBuilder.factory.buildCampaignParticipation({
73+
campaignId: campaign.id,
74+
status: CampaignParticipationStatuses.TO_SHARE,
75+
createdAt: '2024-12-02',
76+
});
77+
78+
await databaseBuilder.commit();
79+
const userIds = await script.fetchUserIds(new Date('2024-12-01'), new Date('2024-12-06'));
80+
expect(userIds).to.have.lengthOf(2);
81+
expect(userIds).to.not.contains(userId);
82+
});
83+
84+
it('should not return the user if the campaign target profile is not included in targeted target profiles', async function () {
85+
const { id: targetProfileId1 } = databaseBuilder.factory.buildTargetProfile({
86+
id: PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS[0],
87+
});
88+
const { id: targetProfileId2 } = databaseBuilder.factory.buildTargetProfile({
89+
id: PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS[1],
90+
});
91+
const { id: targetProfileId3 } = databaseBuilder.factory.buildTargetProfile({
92+
id: PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS[2],
93+
});
94+
const { id: targetProfileId4 } = databaseBuilder.factory.buildTargetProfile();
95+
const campaign1 = databaseBuilder.factory.buildCampaign({ targetProfileId: targetProfileId1 });
96+
const campaign2 = databaseBuilder.factory.buildCampaign({ targetProfileId: targetProfileId2 });
97+
const campaign3 = databaseBuilder.factory.buildCampaign({ targetProfileId: targetProfileId3 });
98+
const campaign4 = databaseBuilder.factory.buildCampaign({ targetProfileId: targetProfileId4 });
99+
const otherParameters = { status: CampaignParticipationStatuses.SHARED, createdAt: '2024-12-02T15:07:57.376Z' };
100+
databaseBuilder.factory.buildCampaignParticipation({
101+
campaignId: campaign1.id,
102+
...otherParameters,
103+
});
104+
databaseBuilder.factory.buildCampaignParticipation({
105+
campaignId: campaign2.id,
106+
...otherParameters,
107+
});
108+
databaseBuilder.factory.buildCampaignParticipation({
109+
campaignId: campaign3.id,
110+
...otherParameters,
111+
});
112+
const { userId: otherTargetProfileCampaignParticipationUserId } =
113+
databaseBuilder.factory.buildCampaignParticipation({
114+
campaignId: campaign4.id,
115+
...otherParameters,
116+
});
117+
118+
await databaseBuilder.commit();
119+
const userIds = await script.fetchUserIds(new Date('2024-12-01'), new Date('2024-12-06'));
120+
expect(userIds).to.have.lengthOf(3);
121+
expect(userIds).to.not.contains(otherTargetProfileCampaignParticipationUserId);
122+
});
123+
124+
it('should return expected users', async function () {
125+
const { id: targetProfileId1 } = databaseBuilder.factory.buildTargetProfile({
126+
id: PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS[0],
127+
});
128+
const { id: targetProfileId2 } = databaseBuilder.factory.buildTargetProfile({
129+
id: PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS[1],
130+
});
131+
const { id: targetProfileId3 } = databaseBuilder.factory.buildTargetProfile({
132+
id: PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS[2],
133+
});
134+
const campaign1 = databaseBuilder.factory.buildCampaign({ targetProfileId: targetProfileId1 });
135+
const campaign2 = databaseBuilder.factory.buildCampaign({ targetProfileId: targetProfileId2 });
136+
const campaign3 = databaseBuilder.factory.buildCampaign({ targetProfileId: targetProfileId3 });
137+
databaseBuilder.factory.buildCampaignParticipation({
138+
campaignId: campaign1.id,
139+
status: CampaignParticipationStatuses.SHARED,
140+
createdAt: '2024-12-01T00:00:01.376Z',
141+
});
142+
databaseBuilder.factory.buildCampaignParticipation({
143+
campaignId: campaign2.id,
144+
status: CampaignParticipationStatuses.TO_SHARE,
145+
createdAt: '2024-12-07T15:07:57.376Z',
146+
});
147+
databaseBuilder.factory.buildCampaignParticipation({
148+
campaignId: campaign3.id,
149+
status: CampaignParticipationStatuses.TO_SHARE,
150+
createdAt: '2024-12-07T23:59:59.376Z',
151+
});
152+
databaseBuilder.factory.buildCampaignParticipation({
153+
campaignId: campaign3.id,
154+
status: CampaignParticipationStatuses.TO_SHARE,
155+
createdAt: '2024-12-31T21:22:00.001Z',
156+
});
157+
databaseBuilder.factory.buildCampaignParticipation({
158+
campaignId: campaign3.id,
159+
status: CampaignParticipationStatuses.TO_SHARE,
160+
createdAt: '2025-01-01T21:22:00.001Z',
161+
});
162+
163+
await databaseBuilder.commit();
164+
const userIds = await script.fetchUserIds(new Date('2024-12-01'), new Date('2024-12-31'));
165+
expect(userIds).to.have.lengthOf(4);
166+
});
167+
});
168+
169+
describe('#handle', function () {
170+
it('should log information for each userId', async function () {
171+
const { id: targetProfileId1 } = databaseBuilder.factory.buildTargetProfile({
172+
id: PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS[2],
173+
});
174+
const { id: targetProfileId2 } = databaseBuilder.factory.buildTargetProfile({
175+
id: PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS[0],
176+
});
177+
const { id: targetProfileId3 } = databaseBuilder.factory.buildTargetProfile({
178+
id: PRODUCTION_SIXTH_GRADE_TARGET_PROFILE_IDS[1],
179+
});
180+
const campaign1 = databaseBuilder.factory.buildCampaign({ targetProfileId: targetProfileId1 });
181+
const campaign2 = databaseBuilder.factory.buildCampaign({ targetProfileId: targetProfileId2 });
182+
const campaign3 = databaseBuilder.factory.buildCampaign({ targetProfileId: targetProfileId3 });
183+
databaseBuilder.factory.buildCampaignParticipation({
184+
campaignId: campaign1.id,
185+
status: CampaignParticipationStatuses.SHARED,
186+
createdAt: '2024-12-01',
187+
});
188+
databaseBuilder.factory.buildCampaignParticipation({
189+
campaignId: campaign2.id,
190+
status: CampaignParticipationStatuses.TO_SHARE,
191+
createdAt: '2024-12-02',
192+
});
193+
databaseBuilder.factory.buildCampaignParticipation({
194+
campaignId: campaign3.id,
195+
status: CampaignParticipationStatuses.TO_SHARE,
196+
createdAt: '2024-12-03',
197+
});
198+
199+
await databaseBuilder.commit();
200+
201+
const script = new SixthGradeAttestationRewardScript();
202+
const logger = { info: sinon.spy(), error: sinon.spy() };
203+
const usecases = { rewardUser: sinon.stub() };
204+
205+
await script.handle({
206+
options: {
207+
start: new Date('2024-12-01'),
208+
end: new Date('2024-12-07'),
209+
},
210+
logger,
211+
rewardUser: usecases.rewardUser,
212+
});
213+
214+
expect(logger.info.callCount).to.equal(4);
215+
});
216+
217+
it('should throw an error if end comes before start.', async function () {
218+
const script = new SixthGradeAttestationRewardScript();
219+
const logger = { info: sinon.spy(), error: sinon.spy() };
220+
const usecases = { rewardUser: sinon.stub() };
221+
222+
const error = await catchErr(script.handle)({
223+
options: {
224+
start: new Date('2024-11-10'),
225+
end: new Date('2024-11-09'),
226+
},
227+
logger,
228+
rewardUser: usecases.rewardUser,
229+
});
230+
231+
expect(error).to.be.an.instanceOf(Error);
232+
});
233+
234+
it('should stop execution if there are no users', async function () {
235+
const script = new SixthGradeAttestationRewardScript();
236+
const logger = { info: sinon.spy(), error: sinon.spy() };
237+
const usecases = { rewardUser: sinon.stub() };
238+
239+
await script.handle({
240+
options: {
241+
start: new Date('2024-12-01'),
242+
end: new Date('2024-12-09'),
243+
},
244+
logger,
245+
rewardUser: usecases.rewardUser,
246+
});
247+
248+
expect(usecases.rewardUser).to.not.have.been.called;
249+
expect(logger.info).to.have.been.calledOnceWithExactly('No user found');
250+
});
251+
});
252+
});

0 commit comments

Comments
 (0)