Skip to content

Commit c945fac

Browse files
laura-bergoensxav-car
authored andcommitted
feat(api): introduce new requirement CappedTubesProfile
1 parent 6a5cca3 commit c945fac

File tree

9 files changed

+385
-8
lines changed

9 files changed

+385
-8
lines changed

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

+45
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const COMPARISONS = {
88
export const TYPES = {
99
COMPOSE: 'compose',
1010
SKILL_PROFILE: 'skillProfile',
11+
CAPPED_TUBES_PROFILE: 'cappedTubesProfile',
1112
OBJECT: {
1213
ORGANIZATION_LEARNER: 'organizationLearner',
1314
ORGANIZATION: 'organization',
@@ -167,6 +168,48 @@ export class SkillProfileRequirement extends BaseRequirement {
167168
}
168169
}
169170

171+
export class CappedTubesProfileRequirement extends BaseRequirement {
172+
#cappedTubes;
173+
#threshold;
174+
175+
constructor({ data }) {
176+
super({ requirement_type: TYPES.CAPPED_TUBES_PROFILE, comparison: null });
177+
this.#cappedTubes = data.cappedTubes;
178+
this.#threshold = data.threshold;
179+
}
180+
181+
/**
182+
* @returns {Object}
183+
*/
184+
get data() {
185+
return {
186+
cappedTubes: Object.freeze(this.#cappedTubes),
187+
threshold: this.#threshold,
188+
};
189+
}
190+
191+
/**
192+
* @param {Eligibility|Success} dataInput
193+
* @returns {Boolean}
194+
*/
195+
isFulfilled(dataInput) {
196+
const masteryPercentage = dataInput.getMasteryPercentageForCappedTubes(this.#cappedTubes);
197+
return masteryPercentage >= this.#threshold;
198+
}
199+
200+
/**
201+
* @returns {Object}
202+
*/
203+
toDTO() {
204+
const superDto = super.toDTO();
205+
delete superDto.comparison;
206+
return {
207+
...superDto,
208+
data: this.data,
209+
};
210+
}
211+
}
212+
170213
/**
171214
* @param {Object} params
172215
* @param {string} params.requirement_type
@@ -182,6 +225,8 @@ export function buildRequirement({ requirement_type, data, comparison }) {
182225
return new ObjectRequirement({ requirement_type, data, comparison });
183226
} else if (requirement_type === TYPES.SKILL_PROFILE) {
184227
return new SkillProfileRequirement({ data });
228+
} else if (requirement_type === TYPES.CAPPED_TUBES_PROFILE) {
229+
return new CappedTubesProfileRequirement({ data });
185230
}
186231
throw new Error(`Unknown requirement_type "${requirement_type}"`);
187232
}

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

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

33
export class Success {
4-
constructor({ knowledgeElements }) {
4+
#dataByTubeId;
5+
constructor({ knowledgeElements, skills }) {
56
this.knowledgeElements = knowledgeElements;
7+
this.skillsForKnowledgeElements = skills;
8+
this.#dataByTubeId = this.knowledgeElements.reduce((acc, nextKe) => {
9+
const skill = this.skillsForKnowledgeElements.find((sk) => sk.id === nextKe.skillId);
10+
if (!acc[skill.tubeId]) {
11+
acc[skill.tubeId] = [];
12+
}
13+
acc[skill.tubeId].push({
14+
isValidated: nextKe.status === KnowledgeElement.StatusType.VALIDATED,
15+
difficulty: skill.difficulty,
16+
});
17+
return acc;
18+
}, {});
619
}
720

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

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

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as knowledgeElementsApi from '../../../evaluation/application/api/knowledge-elements-api.js';
2+
import * as skillsApi from '../../../learning-content/application/api/skills-api.js';
23
import * as organizationLearnerWithParticipationApi from '../../../prescription/organization-learner/application/api/organization-learners-with-participations-api.js';
34
import * as profileRewardApi from '../../../profile/application/api/profile-reward-api.js';
45
import * as rewardApi from '../../../profile/application/api/reward-api.js';
@@ -21,6 +22,7 @@ const repositoriesWithoutInjectedDependencies = {
2122
const dependencies = {
2223
organizationLearnerWithParticipationApi,
2324
knowledgeElementsApi,
25+
skillsApi,
2426
profileRewardApi,
2527
profileRewardTemporaryStorage,
2628
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, knowledgeElementsApi, skillsApi }) => {
44
const knowledgeElements = await knowledgeElementsApi.findFilteredMostRecentByUser({ userId });
5-
return new Success({ knowledgeElements });
5+
const skillIds = knowledgeElements.map((ke) => ke.skillId);
6+
const skills = await skillsApi.findByIds({ ids: skillIds });
7+
return new Success({ knowledgeElements, skills });
68
};

api/tests/quest/integration/domain/usecases/reward-user_test.js

+16-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,14 @@ const setupContext = async (
3737
status: hasValidatedKnowledgeElements ? VALIDATED : INVALIDATED,
3838
},
3939
];
40-
userKnowledgeElements.map(databaseBuilder.factory.buildKnowledgeElement);
40+
userKnowledgeElements.map((ke) => {
41+
databaseBuilder.factory.buildKnowledgeElement(ke);
42+
databaseBuilder.factory.learningContent.buildSkill({
43+
id: ke.skillId,
44+
tubeId: `tubeFor${ke.skillId}`,
45+
difficulty: 1,
46+
});
47+
});
4148

4249
const organization = databaseBuilder.factory.buildOrganization({ type: userOrganization });
4350
const { id: organizationLearnerId } = databaseBuilder.factory.buildOrganizationLearner({
@@ -128,7 +135,14 @@ describe('Quest | Integration | Domain | Usecases | RewardUser', function () {
128135
status: VALIDATED,
129136
},
130137
];
131-
userKnowledgeElements.map(databaseBuilder.factory.buildKnowledgeElement);
138+
userKnowledgeElements.map((ke) => {
139+
databaseBuilder.factory.buildKnowledgeElement(ke);
140+
databaseBuilder.factory.learningContent.buildSkill({
141+
id: ke.skillId,
142+
tubeId: `tubeFor${ke.skillId}`,
143+
difficulty: 1,
144+
});
145+
});
132146

133147
const organization = databaseBuilder.factory.buildOrganization({ type: questOrganization });
134148
const { id: organizationLearnerId } = databaseBuilder.factory.buildOrganizationLearner({

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

+18
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ describe('Quest | Unit | Domain | Models | Quest ', function () {
103103
{ status: KnowledgeElement.StatusType.VALIDATED, skillId: 'skillC' },
104104
{ status: KnowledgeElement.StatusType.VALIDATED, skillId: 'skillD' },
105105
],
106+
skills: [
107+
{ id: 'skillA', tubeId: 'tubeA', difficulty: 1 },
108+
{ id: 'skillB', tubeId: 'tubeA', difficulty: 1 },
109+
{ id: 'skillC', tubeId: 'tubeA', difficulty: 1 },
110+
{ id: 'skillD', tubeId: 'tubeA', difficulty: 1 },
111+
],
106112
});
107113

108114
expect(quest.isSuccessful(success)).to.be.true;
@@ -136,6 +142,12 @@ describe('Quest | Unit | Domain | Models | Quest ', function () {
136142
{ status: KnowledgeElement.StatusType.VALIDATED, skillId: 'skillC' },
137143
{ status: KnowledgeElement.StatusType.VALIDATED, skillId: 'skillD' },
138144
],
145+
skills: [
146+
{ id: 'skillA', tubeId: 'tubeA', difficulty: 1 },
147+
{ id: 'skillB', tubeId: 'tubeA', difficulty: 1 },
148+
{ id: 'skillC', tubeId: 'tubeA', difficulty: 1 },
149+
{ id: 'skillD', tubeId: 'tubeA', difficulty: 1 },
150+
],
139151
});
140152

141153
expect(quest.isSuccessful(success)).to.be.false;
@@ -169,6 +181,12 @@ describe('Quest | Unit | Domain | Models | Quest ', function () {
169181
{ status: KnowledgeElement.StatusType.INVALIDATED, skillId: 'skillC' },
170182
{ status: KnowledgeElement.StatusType.INVALIDATED, skillId: 'skillD' },
171183
],
184+
skills: [
185+
{ id: 'skillA', tubeId: 'tubeA', difficulty: 1 },
186+
{ id: 'skillB', tubeId: 'tubeA', difficulty: 1 },
187+
{ id: 'skillC', tubeId: 'tubeA', difficulty: 1 },
188+
{ id: 'skillD', tubeId: 'tubeA', difficulty: 1 },
189+
],
172190
});
173191

174192
expect(quest.isSuccessful(success)).to.be.false;

0 commit comments

Comments
 (0)