Skip to content

Commit de2d59f

Browse files
laura-bergoensfrinyvonnickxav-car
committed
tech(api): add campaign skills in Success model
Co-authored-by: Yvonnick Frin <yvonnick.frin@pix.fr> Co-authored-by: Xavier Carron <xavier.carron@pix.fr>
1 parent b535029 commit de2d59f

File tree

6 files changed

+189
-9
lines changed

6 files changed

+189
-9
lines changed

api/src/quest/domain/models/Success.js

+35-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import { KnowledgeElement } from '../../../shared/domain/models/index.js';
22

33
export class Success {
4-
constructor({ knowledgeElements }) {
4+
constructor({ knowledgeElements, campaignSkills }) {
55
this.knowledgeElements = knowledgeElements;
6+
this.campaignSkills = campaignSkills;
67
}
78

9+
/**
10+
*
11+
* @param {Array<string>} skillIds
12+
* @returns {number}
13+
*/
814
getMasteryPercentageForSkills(skillIds) {
915
// genre de doublon avec api/src/shared/domain/models/CampaignParticipationResult.js:64
1016
const totalSkillsCount = skillIds?.length;
@@ -16,4 +22,32 @@ export class Success {
1622
).length;
1723
return Math.round((validatedSkillsCount * 100) / totalSkillsCount);
1824
}
25+
26+
/**
27+
*
28+
* @param {Array<{tubeId: string, level: number}>} cappedTubes
29+
* @returns {number}
30+
*/
31+
// Pour cette implémentation, ne tient pas compte des versions d'acquis obtenus dans les campagnes
32+
// potentiellement effectuées dans le cadre de la quête, mais seulement du profil de l'utilisateur
33+
// pour que ça marche efficacement, ajouter une condition d'éligibilité qui impose d'être allé au bout
34+
// de la participation, on s'assure ainsi qu'il a bien un KE pour chaque acquis des campagnes et qu'il
35+
// n'obtienne pas l'attestation sans avoir effectivement participé
36+
37+
// L'autre solution serait de baser les cappedTubes sur l'ensemble des lots d'acquis des campagnes
38+
// concernées peut-être ?
39+
/*getMasteryPercentageForCappedTubes(cappedTubes) {
40+
if (!cappedTubes?.length) {
41+
return 0;
42+
}
43+
44+
let sumTotal = 0;
45+
let sumValidated = 0;
46+
for (const { tubeId, level } of cappedTubes) {
47+
sumTotal += level; // ceci est très cavalier, je présuppose que le référentiel n'a aucun trou \o/
48+
const dataForTubeId = this.#dataByTubeId[tubeId] ?? [];
49+
sumValidated += dataForTubeId.filter(({ isValidated, difficulty }) => isValidated && difficulty <= level).length;
50+
}
51+
return Math.round((sumValidated * 100) / sumTotal);
52+
}*/
1953
}

api/src/quest/infrastructure/repositories/index.js

+4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import * as knowledgeElementsApi from '../../../evaluation/application/api/knowledge-elements-api.js';
2+
import * as skillsApi from '../../../learning-content/application/api/skills-api.js';
3+
import * as campaignsApi from '../../../prescription/campaign/application/api/campaigns-api.js';
24
import * as organizationLearnerWithParticipationApi from '../../../prescription/organization-learner/application/api/organization-learners-with-participations-api.js';
35
import * as profileRewardApi from '../../../profile/application/api/profile-reward-api.js';
46
import * as rewardApi from '../../../profile/application/api/reward-api.js';
@@ -21,6 +23,8 @@ const repositoriesWithoutInjectedDependencies = {
2123
const dependencies = {
2224
organizationLearnerWithParticipationApi,
2325
knowledgeElementsApi,
26+
campaignsApi,
27+
skillsApi,
2428
profileRewardApi,
2529
profileRewardTemporaryStorage,
2630
rewardApi,
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Success } from '../../domain/models/Success.js';
22

3-
export const find = async ({ userId, knowledgeElementsApi }) => {
3+
export const find = async ({ userId, campaignParticipationIds, knowledgeElementsApi, skillsApi, campaignsApi }) => {
44
const knowledgeElements = await knowledgeElementsApi.findFilteredMostRecentByUser({ userId });
5-
return new Success({ knowledgeElements });
5+
const campaignSkillIds = await campaignsApi.findCampaignSkillIdsForCampaignParticipations(campaignParticipationIds);
6+
const campaignSkills = await skillsApi.findByIds({ ids: campaignSkillIds });
7+
return new Success({ knowledgeElements, campaignSkills });
68
};

api/tests/quest/unit/domain/models/Quest_test.js

+18
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,12 @@ describe('Quest | Unit | Domain | Models | Quest ', function () {
107107
{ status: KnowledgeElement.StatusType.VALIDATED, skillId: 'skillC' },
108108
{ status: KnowledgeElement.StatusType.VALIDATED, skillId: 'skillD' },
109109
],
110+
skills: [
111+
{ id: 'skillA', tubeId: 'tubeA', difficulty: 1 },
112+
{ id: 'skillB', tubeId: 'tubeA', difficulty: 1 },
113+
{ id: 'skillC', tubeId: 'tubeA', difficulty: 1 },
114+
{ id: 'skillD', tubeId: 'tubeA', difficulty: 1 },
115+
],
110116
});
111117
const data = new DataForQuest({ success });
112118

@@ -141,6 +147,12 @@ describe('Quest | Unit | Domain | Models | Quest ', function () {
141147
{ status: KnowledgeElement.StatusType.VALIDATED, skillId: 'skillC' },
142148
{ status: KnowledgeElement.StatusType.VALIDATED, skillId: 'skillD' },
143149
],
150+
skills: [
151+
{ id: 'skillA', tubeId: 'tubeA', difficulty: 1 },
152+
{ id: 'skillB', tubeId: 'tubeA', difficulty: 1 },
153+
{ id: 'skillC', tubeId: 'tubeA', difficulty: 1 },
154+
{ id: 'skillD', tubeId: 'tubeA', difficulty: 1 },
155+
],
144156
});
145157
const data = new DataForQuest({ success });
146158

@@ -175,6 +187,12 @@ describe('Quest | Unit | Domain | Models | Quest ', function () {
175187
{ status: KnowledgeElement.StatusType.INVALIDATED, skillId: 'skillC' },
176188
{ status: KnowledgeElement.StatusType.INVALIDATED, skillId: 'skillD' },
177189
],
190+
skills: [
191+
{ id: 'skillA', tubeId: 'tubeA', difficulty: 1 },
192+
{ id: 'skillB', tubeId: 'tubeA', difficulty: 1 },
193+
{ id: 'skillC', tubeId: 'tubeA', difficulty: 1 },
194+
{ id: 'skillD', tubeId: 'tubeA', difficulty: 1 },
195+
],
178196
});
179197
const data = new DataForQuest({ success });
180198

api/tests/quest/unit/domain/models/Success_test.js

+85
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ describe('Quest | Unit | Domain | Models | Success ', function () {
1515
{ status: KnowledgeElement.StatusType.VALIDATED, skillId: 'skillB' },
1616
{ status: KnowledgeElement.StatusType.INVALIDATED, skillId: 'skillC' },
1717
],
18+
skills: [
19+
{ id: 'skillA', tubeId: 'tubeA', difficulty: 1 },
20+
{ id: 'skillB', tubeId: 'tubeB', difficulty: 1 },
21+
{ id: 'skillC', tubeId: 'tubeC', difficulty: 1 },
22+
],
1823
});
1924

2025
// when
@@ -37,6 +42,12 @@ describe('Quest | Unit | Domain | Models | Success ', function () {
3742
{ status: KnowledgeElement.StatusType.INVALIDATED, skillId: 'skillC' },
3843
{ status: KnowledgeElement.StatusType.INVALIDATED, skillId: 'skillD' },
3944
],
45+
skills: [
46+
{ id: 'skillA', tubeId: 'tubeA', difficulty: 1 },
47+
{ id: 'skillB', tubeId: 'tubeB', difficulty: 1 },
48+
{ id: 'skillC', tubeId: 'tubeC', difficulty: 1 },
49+
{ id: 'skillD', tubeId: 'tubeD', difficulty: 1 },
50+
],
4051
});
4152

4253
// when
@@ -48,4 +59,78 @@ describe('Quest | Unit | Domain | Models | Success ', function () {
4859
});
4960
});
5061
});
62+
/*
63+
describe('#getMasteryPercentageForCappedTubes', function () {
64+
context('when no cappedTubes provided', function () {
65+
it('should return 0', function () {
66+
// given
67+
const cappedTubesEmpty = [];
68+
const brokenCappedTubes = null;
69+
const success = new Success({
70+
knowledgeElements: [
71+
{ status: KnowledgeElement.StatusType.VALIDATED, skillId: 'skillA' },
72+
{ status: KnowledgeElement.StatusType.VALIDATED, skillId: 'skillB' },
73+
{ status: KnowledgeElement.StatusType.INVALIDATED, skillId: 'skillC' },
74+
],
75+
skills: [
76+
{ id: 'skillA', tubeId: 'tubeA', difficulty: 1 },
77+
{ id: 'skillB', tubeId: 'tubeB', difficulty: 1 },
78+
{ id: 'skillC', tubeId: 'tubeC', difficulty: 1 },
79+
],
80+
});
81+
82+
// when
83+
const masteryPercentageEmpty = success.getMasteryPercentageForCappedTubes(cappedTubesEmpty);
84+
const masteryPercentageBroken = success.getMasteryPercentageForCappedTubes(brokenCappedTubes);
85+
86+
// then
87+
expect(masteryPercentageEmpty).to.equal(0);
88+
expect(masteryPercentageBroken).to.equal(0);
89+
});
90+
});
91+
context('when cappedTubes are provided', function () {
92+
it('should return the expected mastery percentage according to knowledge elements by tube in Success model', function () {
93+
// given
94+
const success = new Success({
95+
// 1/2 sur tubeA cappé 2
96+
// 2/3 sur tubeB cappé 3
97+
// au final ça donne 3 / 5 -> 60%
98+
knowledgeElements: [
99+
{ status: KnowledgeElement.StatusType.VALIDATED, skillId: 'skill1tubeA' },
100+
{ status: KnowledgeElement.StatusType.INVALIDATED, skillId: 'skill2tubeA' },
101+
{ status: KnowledgeElement.StatusType.VALIDATED, skillId: 'skill3tubeA' }, // ignoré, hors cap
102+
{ status: KnowledgeElement.StatusType.VALIDATED, skillId: 'skill4tubeA' }, // ignoré, hors cap
103+
{ status: KnowledgeElement.StatusType.VALIDATED, skillId: 'skill1tubeB' },
104+
{ status: KnowledgeElement.StatusType.VALIDATED, skillId: 'skill2tubeB' },
105+
{ status: KnowledgeElement.StatusType.INVALIDATED, skillId: 'skill3tubeB' },
106+
{ status: KnowledgeElement.StatusType.VALIDATED, skillId: 'skillTubeC' }, // ignoré, hors scope
107+
{ status: KnowledgeElement.StatusType.VALIDATED, skillId: 'skillTubeD' }, // ignoré, hors scope
108+
],
109+
skills: [
110+
{ id: 'skill1tubeA', tubeId: 'tubeA', difficulty: 1 },
111+
{ id: 'skill2tubeA', tubeId: 'tubeA', difficulty: 2 },
112+
{ id: 'skill3tubeA', tubeId: 'tubeA', difficulty: 3 },
113+
{ id: 'skill4tubeA', tubeId: 'tubeA', difficulty: 4 },
114+
{ id: 'skill1tubeB', tubeId: 'tubeB', difficulty: 1 },
115+
{ id: 'skill2tubeB', tubeId: 'tubeB', difficulty: 2 },
116+
{ id: 'skill3tubeB', tubeId: 'tubeB', difficulty: 3 },
117+
{ id: 'skillTubeC', tubeId: 'tubeC', difficulty: 1 },
118+
{ id: 'skillTubeD', tubeId: 'tubeD', difficulty: 1 },
119+
],
120+
});
121+
122+
// when
123+
const cappedTubes = [
124+
{ tubeId: 'tubeA', level: 2 },
125+
{ tubeId: 'tubeB', level: 3 },
126+
];
127+
const masteryPercentage = success.getMasteryPercentageForCappedTubes(cappedTubes);
128+
129+
// then
130+
const expectedMasteryPercentage = 60;
131+
expect(masteryPercentage).to.be.equal(expectedMasteryPercentage);
132+
});
133+
});
134+
});
135+
*/
51136
});

api/tests/quest/unit/infrastructure/repositories/success-repository_test.js

+43-6
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,65 @@ import sinon from 'sinon';
22

33
import { Success } from '../../../../../src/quest/domain/models/Success.js';
44
import * as successRepository from '../../../../../src/quest/infrastructure/repositories/success-repository.js';
5-
import { expect } from '../../../../test-helper.js';
5+
import { expect, preventStubsToBeCalledUnexpectedly } from '../../../../test-helper.js';
66

77
describe('Quest | Unit | Infrastructure | repositories | success', function () {
88
describe('#find', function () {
9-
it('should call Knowledge Elements API', async function () {
9+
let knowledgeElementsApi_findFilteredMostRecentByUserStub;
10+
let skillsApi_findByIdsStub;
11+
let campaignsApi_findCampaignSkillIdsForCampaignParticipationsStub;
12+
13+
beforeEach(function () {
14+
knowledgeElementsApi_findFilteredMostRecentByUserStub = sinon.stub().named('findFilteredMostRecentByUser');
15+
skillsApi_findByIdsStub = sinon.stub().named('findByIds');
16+
campaignsApi_findCampaignSkillIdsForCampaignParticipationsStub = sinon
17+
.stub()
18+
.named('findCampaignSkillIdsForCampaignParticipations');
19+
preventStubsToBeCalledUnexpectedly([
20+
knowledgeElementsApi_findFilteredMostRecentByUserStub,
21+
skillsApi_findByIdsStub,
22+
campaignsApi_findCampaignSkillIdsForCampaignParticipationsStub,
23+
]);
24+
});
25+
26+
it('should return a Success model according to data fetched from diverse APIs', async function () {
1027
// given
1128
const userId = Symbol('userId');
12-
const knowledgeElements = Symbol('knowledgeElements');
29+
const knowledgeElements = [{ skillId: 'A' }, { skillId: 'B' }];
30+
const campaignParticipationIds = Symbol('campaignParticipationIds');
31+
const campaignSkillIds = Symbol('campaignSkillIds');
32+
const skills = [
33+
{ id: 'A', tubeId: 'AA' },
34+
{ id: 'B', tubeId: 'BB' },
35+
];
1336
const knowledgeElementsApi = {
14-
findFilteredMostRecentByUser: sinon.stub(),
37+
findFilteredMostRecentByUser: knowledgeElementsApi_findFilteredMostRecentByUserStub,
38+
};
39+
const skillsApi = {
40+
findByIds: skillsApi_findByIdsStub,
41+
};
42+
const campaignsApi = {
43+
findCampaignSkillIdsForCampaignParticipations: campaignsApi_findCampaignSkillIdsForCampaignParticipationsStub,
1544
};
16-
knowledgeElementsApi.findFilteredMostRecentByUser.withArgs({ userId }).resolves(knowledgeElements);
45+
knowledgeElementsApi_findFilteredMostRecentByUserStub.withArgs({ userId }).resolves(knowledgeElements);
46+
campaignsApi.findCampaignSkillIdsForCampaignParticipations
47+
.withArgs(campaignParticipationIds)
48+
.resolves(campaignSkillIds);
49+
skillsApi_findByIdsStub.withArgs({ ids: campaignSkillIds }).resolves(skills);
1750

1851
// when
1952
const result = await successRepository.find({
2053
userId,
54+
campaignParticipationIds,
2155
knowledgeElementsApi,
56+
campaignsApi,
57+
skillsApi,
2258
});
2359

2460
// then
2561
expect(result).to.be.an.instanceof(Success);
26-
expect(result.knowledgeElements).to.equal(knowledgeElements);
62+
expect(result.knowledgeElements).to.deepEqualArray(knowledgeElements);
63+
expect(result.skillsForKnowledgeElements).to.equal(skills);
2764
});
2865
});
2966
});

0 commit comments

Comments
 (0)