Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TECH] Expliciter la définiton des quêtes (PIX-16445) #11347

Open
wants to merge 7 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions api/db/seeds/data/team-prescription/build-quests.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ATTESTATIONS } from '../../../../src/profile/domain/constants.js';
import { REWARD_TYPES } from '../../../../src/quest/domain/constants.js';
import { TYPES } from '../../../../src/quest/domain/models/Eligibility.js';
import { COMPARISON } from '../../../../src/quest/domain/models/Quest.js';
import { Assessment, CampaignParticipationStatuses, Membership } from '../../../../src/shared/domain/models/index.js';
import { temporaryStorage } from '../../../../src/shared/infrastructure/key-value-storages/index.js';
Expand Down Expand Up @@ -157,22 +158,22 @@ const buildSixthGradeQuests = (
) => {
const firstQuestRequirement = [
{
type: 'organization',
type: TYPES.ORGANIZATION,
data: {
type: 'SCO',
},
comparison: COMPARISON.ALL,
},
{
type: 'organization',
type: TYPES.ORGANIZATION,
data: {
isManagingStudents: true,
tags: [AEFE_TAG.name],
},
comparison: COMPARISON.ONE_OF,
},
{
type: 'campaignParticipations',
type: TYPES.CAMPAIGN_PARTICIPATIONS,
data: {
targetProfileIds: [firstTargetProfile.id],
},
Expand All @@ -198,22 +199,22 @@ const buildSixthGradeQuests = (

const secondQuestEligibilityRequirements = [
{
type: 'organization',
type: TYPES.ORGANIZATION,
data: {
type: 'SCO',
},
comparison: COMPARISON.ALL,
},
{
type: 'organization',
type: TYPES.ORGANIZATION,
data: {
isManagingStudents: true,
tags: [AEFE_TAG.name],
},
comparison: COMPARISON.ONE_OF,
},
{
type: 'campaignParticipations',
type: TYPES.CAMPAIGN_PARTICIPATIONS,
data: {
targetProfileIds: [secondTargetProfile.id, thirdTargetProfile.id],
},
Expand Down
45 changes: 8 additions & 37 deletions api/src/quest/domain/models/Eligibility.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,16 @@
export class Eligibility {
#campaignParticipations;
export const TYPES = {
ORGANIZATION_LEARNER: 'organizationLearner',
ORGANIZATION: 'organization',
CAMPAIGN_PARTICIPATIONS: 'campaignParticipations',
};

export class Eligibility {
constructor({ organizationLearner, organization, campaignParticipations = [] }) {
this.organizationLearner = {
id: organizationLearner?.id,
MEFCode: organizationLearner?.MEFCode,
};
this.organization = organization;
this.#campaignParticipations = campaignParticipations;
}

get campaignParticipations() {
return {
targetProfileIds: this.#campaignParticipations.map(({ targetProfileId }) => targetProfileId),
};
}

set campaignParticipations(campaignParticipations) {
this.#campaignParticipations = campaignParticipations;
}

hasCampaignParticipation(campaignParticipationId) {
return Boolean(
this.#campaignParticipations.find(
(campaignParticipation) => campaignParticipation.id === campaignParticipationId,
),
);
}

hasCampaignParticipationForTargetProfileId(targetProfileId) {
return Boolean(
this.#campaignParticipations.find(
(campaignParticipation) => campaignParticipation.targetProfileId === targetProfileId,
),
);
}

getTargetProfileForCampaignParticipation(campaignParticipationId) {
const campaignParticipation = this.#campaignParticipations.find(
(campaignParticipation) => campaignParticipation.id === campaignParticipationId,
);

return campaignParticipation?.targetProfileId ?? null;
this.campaignParticipations = campaignParticipations;
}
}
67 changes: 59 additions & 8 deletions api/src/quest/domain/models/Quest.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { KnowledgeElement } from '../../../shared/domain/models/index.js';
import { TYPES as ELIGIBILITY_TYPES } from './Eligibility.js';

export const COMPARISON = {
ALL: 'all',
Expand All @@ -15,26 +16,76 @@ class Quest {
this.successRequirements = successRequirements;
}

/**
* @param {Eligibility} eligibility
* @param {number} campaignParticipationId
*/
isGrantedWithParticipationId({ eligibility, campaignParticipationId }) {
const criteria = this.eligibilityRequirements.filter(
(eligibilityRequirement) => eligibilityRequirement.type === ELIGIBILITY_TYPES.CAMPAIGN_PARTICIPATIONS,
);

if (criteria.length === 0) return true;

const campaignParticipation = eligibility.campaignParticipations.find(
(campaignParticipation) => campaignParticipation.id === campaignParticipationId,
);

for (const criterion of criteria) {
const alterKey = criterion.data.targetProfileIds !== undefined ? 'targetProfileIds' : 'targetProfileId';

const isQuestRelatedToCampaignParticipationId = criterion.data[alterKey].includes(
campaignParticipation.targetProfileId,
);

if (isQuestRelatedToCampaignParticipationId) return true;
}

return false;
}

/**
* @param {Eligibility} eligibility
*/
isEligible(eligibility) {
if (this.eligibilityRequirements.length === 0) return false;

return this.eligibilityRequirements.every((eligibilityRequirement) =>
this.#checkRequirement(eligibilityRequirement, eligibility),
);
}

#checkCriterion({ criterion, eligibilityData }) {
if (Array.isArray(criterion)) {
if (Array.isArray(eligibilityData)) {
return criterion.every((valueToTest) => eligibilityData.includes(valueToTest));
}
return criterion.some((valueToTest) => valueToTest === eligibilityData);
}
return eligibilityData === criterion;
}

#checkRequirement(eligibilityRequirement, eligibility) {
const comparaisonFunction = eligibilityRequirement.comparison === COMPARISON.ONE_OF ? 'some' : 'every';
const comparisonFunction = eligibilityRequirement.comparison === COMPARISON.ONE_OF ? 'some' : 'every';

return Object.keys(eligibilityRequirement.data)[comparaisonFunction]((key) => {
const eligibilityData = eligibility[eligibilityRequirement.type][key];
const criterion = eligibilityRequirement.data[key];
if (Array.isArray(eligibility[eligibilityRequirement.type])) {
return eligibility[eligibilityRequirement.type].some((item) => {
return Object.keys(eligibilityRequirement.data)[comparisonFunction]((key) => {
// TODO: Dés que les quêtes ont été mises à jour il faudra retirer cette ligne
const alterKey = key === 'targetProfileIds' ? 'targetProfileId' : key;
return this.#checkCriterion({
criterion: eligibilityRequirement.data[key],
eligibilityData: item[alterKey],
});
});
});
}

if (Array.isArray(criterion)) {
return criterion.every((valueToTest) => eligibilityData.includes(valueToTest));
}
return eligibilityData === criterion;
return Object.keys(eligibilityRequirement.data)[comparisonFunction]((key) => {
return this.#checkCriterion({
criterion: eligibilityRequirement.data[key],
eligibilityData: eligibility[eligibilityRequirement.type][key],
});
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,105 +1,33 @@
const getEligibilityForThisCampaignParticipation = async (eligibilityRepository, userId, campaignParticipationId) => {
const eligibilities = await eligibilityRepository.find({ userId });
return eligibilities.find((e) => e.hasCampaignParticipation(campaignParticipationId));
};

const getTargetProfileRequirementsPerQuest = (quests) =>
quests
.map((quest) => {
const campaignParticipationsRequirement = quest.eligibilityRequirements.find(
(requirement) => requirement.type === 'campaignParticipations',
);
if (campaignParticipationsRequirement && campaignParticipationsRequirement.data.targetProfileIds)
return campaignParticipationsRequirement.data.targetProfileIds;
})
.filter(Boolean);

/**
* This function retrieves the target profiles we should use for the current participation.
* It first retrieves the target profile for the current campaign participation.
* Then it retrieves the target profile requirements for each quest.
* It filters the target profile requirements to only keep the ones that contain the target profile for the current participation.
* It checks if the user has participated in campaigns linked to all the target profiles present in the quest requirements.
* If the user has participated in campaigns linked to all the target profiles present in the quest requirements, it returns the target profile requirements containing the target profile for the current participation.
* If not, it returns the target profile for the current participation.
*
* @param campaignParticipationRepository
* @param {number} campaignParticipationId
* @param {[Quest]} quests
* @param {Eligibility} eligibility
* @returns {Promise<[number]>}
*/
const getTargetProfilesForThisCampaignParticipation = async ({
campaignParticipationRepository,
campaignParticipationId,
quests,
eligibility,
}) => {
const { targetProfileId: targetProfileForThisParticipation } =
await campaignParticipationRepository.getCampaignByParticipationId({
campaignParticipationId,
});

const targetProfileRequirementsPerQuest = getTargetProfileRequirementsPerQuest(quests);

const targetProfileRequirementsContainingTargetProfileForCurrentParticipation =
targetProfileRequirementsPerQuest.filter((targetProfileIds) =>
targetProfileIds.includes(targetProfileForThisParticipation),
);

const targetProfileRequirementContainingTargetProfileForCurrentParticipationWithParticipationForEveryTargetProfile =
targetProfileRequirementsContainingTargetProfileForCurrentParticipation.find((targetProfileRequirement) =>
targetProfileRequirement.every((targetProfileId) =>
eligibility.hasCampaignParticipationForTargetProfileId(targetProfileId),
),
);

return (
targetProfileRequirementContainingTargetProfileForCurrentParticipationWithParticipationForEveryTargetProfile ?? [
targetProfileForThisParticipation,
]
);
};
import * as campaignParticipationRepository from '../../../prescription/campaign-participation/infrastructure/repositories/campaign-participation-repository.js';

export const getQuestResultsForCampaignParticipation = async ({
userId,
campaignParticipationId,
dirtyImportcampaignParticipationRepository = campaignParticipationRepository,
questRepository,
eligibilityRepository,
rewardRepository,
campaignParticipationRepository,
}) => {
const quests = await questRepository.findAll();

if (quests.length === 0) {
return [];
}

const eligibility = await getEligibilityForThisCampaignParticipation(
eligibilityRepository,
userId,
campaignParticipationId,
);
const campaignParticipation = await dirtyImportcampaignParticipationRepository.get(campaignParticipationId);

if (!eligibility) return [];
const eligibilities = await eligibilityRepository.find({ userId });

const targetProfileIdsForThisCampaignParticipation = await getTargetProfilesForThisCampaignParticipation({
campaignParticipationRepository,
campaignParticipationId,
quests,
eligibility,
});
const eligibility = eligibilities.find(
(eligibility) => eligibility.organizationLearner.id === campaignParticipation.organizationLearnerId,
);

eligibility.campaignParticipations = targetProfileIdsForThisCampaignParticipation.map((targetProfileId) => ({
targetProfileId,
}));
const questsRelatedToCampaignParticipation = quests.filter(
(q) => q.isGrantedWithParticipationId({ eligibility, campaignParticipationId }) && q.isEligible(eligibility),
);

const questResults = [];
for (const quest of quests) {
const isEligibleForQuest = quest.isEligible(eligibility);

if (!isEligibleForQuest) continue;

for (const quest of questsRelatedToCampaignParticipation) {
const questResult = await rewardRepository.getByQuestAndUserId({ userId, quest });
questResults.push(questResult);
}
Expand Down
3 changes: 3 additions & 0 deletions api/tests/quest/acceptance/application/quest-route_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ describe('Quest | Acceptance | Application | Quest Route ', function () {

// then
expect(response.statusCode).to.equal(200);
// TODO: it seems to be 0 if we refer to unit test of
// Quest | Unit | Domain | Usecases | getQuestResultsForCampaignParticipation
// should return empty array when there is no eligibility ? So can you explain this please
expect(response.result.data).to.have.lengthOf(1);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,14 @@ describe('Quest | Integration | Domain | Usecases | getQuestResultsForCampaignPa
{
type: 'campaignParticipations',
data: {
targetProfileIds: [firstTargetProfile.id, secondTargetProfile.id],
targetProfileIds: [firstTargetProfile.id],
},
comparison: COMPARISON.ALL,
},
{
type: 'campaignParticipations',
data: {
targetProfileIds: [secondTargetProfile.id],
},
comparison: COMPARISON.ALL,
},
Expand Down Expand Up @@ -146,7 +153,21 @@ describe('Quest | Integration | Domain | Usecases | getQuestResultsForCampaignPa
{
type: 'campaignParticipations',
data: {
targetProfileIds: [firstTargetProfile.id, secondTargetProfile.id, thirdTargetProfile.id],
targetProfileIds: [firstTargetProfile.id],
},
comparison: COMPARISON.ALL,
},
{
type: 'campaignParticipations',
data: {
targetProfileIds: [secondTargetProfile.id],
},
comparison: COMPARISON.ALL,
},
{
type: 'campaignParticipations',
data: {
targetProfileIds: [thirdTargetProfile.id],
},
comparison: COMPARISON.ALL,
},
Expand Down
Loading