Skip to content

Commit 5445cd1

Browse files
feat(api): introduce new requirement CappedTubesProfile
1 parent f2e2f44 commit 5445cd1

File tree

11 files changed

+396
-19
lines changed

11 files changed

+396
-19
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/create-or-update-quests-in-batch_test.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ describe('Integration | Quest | Domain | UseCases | create-or-update-quests-in-b
4545
filePath = await createTempFile(
4646
'test.csv',
4747
`Quest ID;Json configuration for quest;deleteQuest
48-
3;{"rewardType":"coucou","rewardId":null,"eligibilityRequirements":{"eligibility":"eligibility"},"successRequirements":{"success":"success"}};OUI
49-
5;{"rewardType":"bonjour","rewardId":null,"eligibilityRequirements":{"eligibility":"une autre eli"},"successRequirements":{"success":"une autre success"}};oui
50-
4;{"rewardType":"salut","rewardId":null,"eligibilityRequirements":{"eligibility":"some other eli"},"successRequirements":{"success":"some other success"}};Non
48+
3;{"rewardType":"coucou","rewardId":null,"eligibilityRequirements":[],"successRequirements":[]};OUI
49+
5;{"rewardType":"bonjour","rewardId":null,"eligibilityRequirements":[],"successRequirements":[]};oui
50+
4;{"rewardType":"salut","rewardId":null,"eligibilityRequirements":[],"successRequirements":[]};Non
5151
`,
5252
);
5353
const spySave = sinon.spy(repositories.questRepository, 'saveInBatch');
@@ -65,8 +65,8 @@ describe('Integration | Quest | Domain | UseCases | create-or-update-quests-in-b
6565
updatedAt: undefined,
6666
rewardType: 'salut',
6767
rewardId: null,
68-
eligibilityRequirements: { eligibility: 'some other eli' },
69-
successRequirements: { success: 'some other success' },
68+
eligibilityRequirements: [],
69+
successRequirements: [],
7070
}),
7171
],
7272
});

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/integration/infrastructure/repositories/quest-repository_test.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -96,22 +96,22 @@ describe('Quest | Integration | Repository | quest', function () {
9696
id: 1,
9797
rewardType: REWARD_TYPES.ATTESTATION,
9898
rewardId: 2,
99-
eligibilityRequirements: { toto: 'tata' },
100-
successRequirements: { titi: 'tutu' },
99+
eligibilityRequirements: [],
100+
successRequirements: [],
101101
});
102102
databaseBuilder.factory.buildQuest({
103103
id: 2,
104104
rewardType: REWARD_TYPES.ATTESTATION,
105105
rewardId: 2,
106-
eligibilityRequirements: { toto: 'tata' },
107-
successRequirements: { titi: 'tutu' },
106+
eligibilityRequirements: [],
107+
successRequirements: [],
108108
});
109109
databaseBuilder.factory.buildQuest({
110110
id: 3,
111111
rewardType: REWARD_TYPES.ATTESTATION,
112112
rewardId: 2,
113-
eligibilityRequirements: { toto: 'tata' },
114-
successRequirements: { titi: 'tutu' },
113+
eligibilityRequirements: [],
114+
successRequirements: [],
115115
});
116116
await databaseBuilder.commit();
117117
await databaseBuilder.fixSequences();

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

+18
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ describe('Quest | Unit | Domain | Models | Quest ', function () {
8585
{ status: KnowledgeElement.StatusType.VALIDATED, skillId: 'skillC' },
8686
{ status: KnowledgeElement.StatusType.VALIDATED, skillId: 'skillD' },
8787
],
88+
skills: [
89+
{ id: 'skillA', tubeId: 'tubeA', difficulty: 1 },
90+
{ id: 'skillB', tubeId: 'tubeA', difficulty: 1 },
91+
{ id: 'skillC', tubeId: 'tubeA', difficulty: 1 },
92+
{ id: 'skillD', tubeId: 'tubeA', difficulty: 1 },
93+
],
8894
});
8995

9096
expect(quest.isSuccessful(success)).to.be.true;
@@ -118,6 +124,12 @@ describe('Quest | Unit | Domain | Models | Quest ', function () {
118124
{ status: KnowledgeElement.StatusType.VALIDATED, skillId: 'skillC' },
119125
{ status: KnowledgeElement.StatusType.VALIDATED, skillId: 'skillD' },
120126
],
127+
skills: [
128+
{ id: 'skillA', tubeId: 'tubeA', difficulty: 1 },
129+
{ id: 'skillB', tubeId: 'tubeA', difficulty: 1 },
130+
{ id: 'skillC', tubeId: 'tubeA', difficulty: 1 },
131+
{ id: 'skillD', tubeId: 'tubeA', difficulty: 1 },
132+
],
121133
});
122134

123135
expect(quest.isSuccessful(success)).to.be.false;
@@ -151,6 +163,12 @@ describe('Quest | Unit | Domain | Models | Quest ', function () {
151163
{ status: KnowledgeElement.StatusType.INVALIDATED, skillId: 'skillC' },
152164
{ status: KnowledgeElement.StatusType.INVALIDATED, skillId: 'skillD' },
153165
],
166+
skills: [
167+
{ id: 'skillA', tubeId: 'tubeA', difficulty: 1 },
168+
{ id: 'skillB', tubeId: 'tubeA', difficulty: 1 },
169+
{ id: 'skillC', tubeId: 'tubeA', difficulty: 1 },
170+
{ id: 'skillD', tubeId: 'tubeA', difficulty: 1 },
171+
],
154172
});
155173

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

0 commit comments

Comments
 (0)