diff --git a/api/db/seeds/data/team-prescription/build-quests.js b/api/db/seeds/data/team-prescription/build-quests.js index f24ab78dc4c..9b035875783 100644 --- a/api/db/seeds/data/team-prescription/build-quests.js +++ b/api/db/seeds/data/team-prescription/build-quests.js @@ -1,6 +1,8 @@ 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 { TYPES as SUCCESS_TYPES } from '../../../../src/quest/domain/models/Success.js'; import { Assessment, CampaignParticipationStatuses, Membership } from '../../../../src/shared/domain/models/index.js'; import { temporaryStorage } from '../../../../src/shared/infrastructure/key-value-storages/index.js'; import { @@ -10,10 +12,35 @@ import { USER_ID_ADMIN_ORGANIZATION, USER_ID_MEMBER_ORGANIZATION, } from '../common/constants.js'; -import { TARGET_PROFILE_BADGES_STAGES_ID } from './constants.js'; +import { TARGET_PROFILE_BADGES_STAGES_ID, TARGET_PROFILE_NO_BADGES_NO_STAGES_ID } from './constants.js'; const profileRewardTemporaryStorage = temporaryStorage.withPrefix('profile-rewards:'); +function buildParenthoodQuest(databaseBuilder) { + const { id: rewardId } = databaseBuilder.factory.buildAttestation({ + templateName: 'parenthood-attestation-template', + key: ATTESTATIONS.PARENTHOOD, + }); + + const targetProfileId = TARGET_PROFILE_NO_BADGES_NO_STAGES_ID; + databaseBuilder.factory.buildQuest({ + rewardType: REWARD_TYPES.ATTESTATION, + rewardId, + eligibilityRequirements: [ + { + type: TYPES.CAMPAIGN_PARTICIPATIONS, + data: { + targetProfileId: [targetProfileId], + status: { + value: [CampaignParticipationStatuses.SHARED, CampaignParticipationStatuses.TO_SHARE], + comparison: COMPARISON.ONE_OF, + }, + }, + }, + ], + }); +} + const USERS = [ { firstName: 'attestation-success', @@ -157,14 +184,14 @@ 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], @@ -172,7 +199,7 @@ const buildSixthGradeQuests = ( comparison: COMPARISON.ONE_OF, }, { - type: 'campaignParticipations', + type: TYPES.CAMPAIGN_PARTICIPATIONS, data: { targetProfileIds: [firstTargetProfile.id], }, @@ -181,7 +208,7 @@ const buildSixthGradeQuests = ( ]; const firstQuestSuccessRequirements = [ { - type: 'skill', + type: SUCCESS_TYPES.SKILL, data: { ids: CAMPAIGN_SKILLS[0], threshold: 50, @@ -198,14 +225,14 @@ 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], @@ -213,7 +240,7 @@ const buildSixthGradeQuests = ( comparison: COMPARISON.ONE_OF, }, { - type: 'campaignParticipations', + type: TYPES.CAMPAIGN_PARTICIPATIONS, data: { targetProfileIds: [secondTargetProfile.id, thirdTargetProfile.id], }, @@ -223,7 +250,7 @@ const buildSixthGradeQuests = ( const secondQuestSuccessRequirements = [ { - type: 'skill', + type: SUCCESS_TYPES.SKILL, data: { ids: [CAMPAIGN_SKILLS[1], CAMPAIGN_SKILLS[2]].flat(), threshold: 50, @@ -440,4 +467,6 @@ export const buildQuests = async (databaseBuilder) => { organizationId: SCO_ORGANIZATION_ID, profileRewardId: otherUserProfileRewardId, }); + + buildParenthoodQuest(databaseBuilder); }; diff --git a/api/src/evaluation/application/api/models/KnowledgeElementDTO.js b/api/src/evaluation/application/api/models/KnowledgeElementDTO.js index 137fb328343..ef1d0ccf4f1 100644 --- a/api/src/evaluation/application/api/models/KnowledgeElementDTO.js +++ b/api/src/evaluation/application/api/models/KnowledgeElementDTO.js @@ -1,5 +1,6 @@ export class KnowledgeElementDTO { - constructor({ status }) { + constructor({ status, skillId }) { this.status = status; + this.skillId = skillId; } } diff --git a/api/src/prescription/organization-learner/application/api/read-models/OrganizationLearnerWithParticipations.js b/api/src/prescription/organization-learner/application/api/read-models/OrganizationLearnerWithParticipations.js index 84b654d6b53..9a755f18417 100644 --- a/api/src/prescription/organization-learner/application/api/read-models/OrganizationLearnerWithParticipations.js +++ b/api/src/prescription/organization-learner/application/api/read-models/OrganizationLearnerWithParticipations.js @@ -10,6 +10,10 @@ export class OrganizationLearnerWithParticipations { tags: tagNames, type: organization.type, }; - this.campaignParticipations = campaignParticipations.map(({ id, targetProfileId }) => ({ id, targetProfileId })); + this.campaignParticipations = campaignParticipations.map(({ id, targetProfileId, status }) => ({ + id, + targetProfileId, + status, + })); } } diff --git a/api/src/profile/domain/constants.js b/api/src/profile/domain/constants.js index 06c4b94a663..e576d4cc2aa 100644 --- a/api/src/profile/domain/constants.js +++ b/api/src/profile/domain/constants.js @@ -1,3 +1,4 @@ export const ATTESTATIONS = { SIXTH_GRADE: 'SIXTH_GRADE', + PARENTHOOD: 'PARENTHOOD', }; diff --git a/api/src/profile/infrastructure/serializers/pdf/templates/parenthood-attestation-template.pdf b/api/src/profile/infrastructure/serializers/pdf/templates/parenthood-attestation-template.pdf new file mode 100644 index 00000000000..143765667f8 Binary files /dev/null and b/api/src/profile/infrastructure/serializers/pdf/templates/parenthood-attestation-template.pdf differ diff --git a/api/src/quest/domain/models/Eligibility.js b/api/src/quest/domain/models/Eligibility.js index 481fbdfde4f..7dc2d6074e7 100644 --- a/api/src/quest/domain/models/Eligibility.js +++ b/api/src/quest/domain/models/Eligibility.js @@ -1,42 +1,35 @@ -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; + this.campaignParticipations = campaignParticipations; } hasCampaignParticipation(campaignParticipationId) { return Boolean( - this.#campaignParticipations.find( - (campaignParticipation) => campaignParticipation.id === campaignParticipationId, - ), + this.campaignParticipations.find((campaignParticipation) => campaignParticipation.id === campaignParticipationId), ); } hasCampaignParticipationForTargetProfileId(targetProfileId) { return Boolean( - this.#campaignParticipations.find( + this.campaignParticipations.find( (campaignParticipation) => campaignParticipation.targetProfileId === targetProfileId, ), ); } getTargetProfileForCampaignParticipation(campaignParticipationId) { - const campaignParticipation = this.#campaignParticipations.find( + const campaignParticipation = this.campaignParticipations.find( (campaignParticipation) => campaignParticipation.id === campaignParticipationId, ); diff --git a/api/src/quest/domain/models/Quest.js b/api/src/quest/domain/models/Quest.js index c7279562056..cb571ed8abe 100644 --- a/api/src/quest/domain/models/Quest.js +++ b/api/src/quest/domain/models/Quest.js @@ -1,4 +1,6 @@ import { KnowledgeElement } from '../../../shared/domain/models/index.js'; +import { TYPES as ELIGIBILITY_TYPES } from './Eligibility.js'; +import { TYPES as SUCCESS_TYPES } from './Success.js'; export const COMPARISON = { ALL: 'all', @@ -24,17 +26,66 @@ class Quest { ); } - #checkRequirement(eligibilityRequirement, eligibility) { - const comparaisonFunction = eligibilityRequirement.comparison === COMPARISON.ONE_OF ? 'some' : 'every'; + isGrantedWithParticipationId({ eligibility, campaignParticipationId }) { + console.log('eligibility', JSON.stringify(eligibility, undefined, 2)); + const criteria = this.eligibilityRequirements.filter( + (eligibilityRequirement) => eligibilityRequirement.type === ELIGIBILITY_TYPES.CAMPAIGN_PARTICIPATIONS, + ); + const campaignParticipation = eligibility.campaignParticipations.find( + (campaignParticipation) => campaignParticipation.id === campaignParticipationId, + ); + console.log('campaignParticipation', JSON.stringify(campaignParticipation, undefined, 2)); - return Object.keys(eligibilityRequirement.data)[comparaisonFunction]((key) => { - const eligibilityData = eligibility[eligibilityRequirement.type][key]; - const criterion = eligibilityRequirement.data[key]; + for (const criterion of criteria) { + console.log('criterion', JSON.stringify(criterion, undefined, 2)); + const alterKey = criterion.data.targetProfileIds !== undefined ? 'targetProfileIds' : 'targetProfileId'; + console.log({ alterKey }); + const isQuestRelatedToCampaignParticipationId = criterion.data[alterKey].includes( + campaignParticipation.targetProfileId, + ); + console.log(isQuestRelatedToCampaignParticipationId); - if (Array.isArray(criterion)) { + if (isQuestRelatedToCampaignParticipationId) return true; + } + + return false; + } + + #checkCriterion({ criterion, eligibilityData }) { + if (Array.isArray(criterion)) { + if (Array.isArray(eligibilityData)) { return criterion.every((valueToTest) => eligibilityData.includes(valueToTest)); } - return eligibilityData === criterion; + return criterion.some((valueToTest) => valueToTest === eligibilityData); + } + if (typeof criterion === 'object') { + const comparisonFunction = criterion.comparison === COMPARISON.ONE_OF ? 'some' : 'every'; + return criterion.value[comparisonFunction]((valueToTest) => eligibilityData.includes(valueToTest)); + } + return eligibilityData === criterion; + } + + #checkRequirement(eligibilityRequirement, eligibility) { + const comparisonFunction = eligibilityRequirement.comparison === COMPARISON.ONE_OF ? 'some' : 'every'; + + 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[alterKey], + eligibilityData: item[key], + }); + }); + }); + } + + return Object.keys(eligibilityRequirement.data)[comparisonFunction]((key) => { + return this.#checkCriterion({ + criterion: eligibilityRequirement.data[key], + eligibilityData: eligibility[eligibilityRequirement.type][key], + }); }); } @@ -42,11 +93,25 @@ class Quest { * @param {Success} success */ isSuccessful(success) { - const skillsCount = this.successRequirements[0].data.ids.length; - const threshold = this.successRequirements[0].data.threshold / 100; - const skillsValidatedCount = success.knowledgeElements.filter( - (knowledgeElement) => knowledgeElement.status === KnowledgeElement.StatusType.VALIDATED, - ).length; + if (this.successRequirements === undefined || this.successRequirements.length === 0) return true; + + return this.successRequirements.every((successRequirement) => { + if (successRequirement.type === SUCCESS_TYPES.SKILL) { + return this.#validateSuccessRequirementsOfTypeSkill({ successRequirement, success }); + } + }); + } + + #validateSuccessRequirementsOfTypeSkill({ successRequirement, success }) { + const knowledgeElementValidatedForSuccess = success.knowledgeElements.filter( + (knowledgeElement) => + successRequirement.data.ids.includes(knowledgeElement.skillId) && + knowledgeElement.status === KnowledgeElement.StatusType.VALIDATED, + ); + const skillsCount = successRequirement.data.ids.length; + const threshold = successRequirement.data.threshold / 100; + + const skillsValidatedCount = knowledgeElementValidatedForSuccess.length; return skillsValidatedCount / skillsCount >= threshold; } diff --git a/api/src/quest/domain/models/Success.js b/api/src/quest/domain/models/Success.js index 22203332a36..a54491445ea 100644 --- a/api/src/quest/domain/models/Success.js +++ b/api/src/quest/domain/models/Success.js @@ -1,3 +1,7 @@ +export const TYPES = { + SKILL: 'skill', +}; + export class Success { constructor({ knowledgeElements }) { this.knowledgeElements = knowledgeElements; diff --git a/api/src/quest/domain/usecases/get-quest-results-for-campaign-participation.js b/api/src/quest/domain/usecases/get-quest-results-for-campaign-participation.js index a70e78f920e..c9036bcfd26 100644 --- a/api/src/quest/domain/usecases/get-quest-results-for-campaign-participation.js +++ b/api/src/quest/domain/usecases/get-quest-results-for-campaign-participation.js @@ -1,65 +1,4 @@ -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, @@ -67,7 +6,6 @@ export const getQuestResultsForCampaignParticipation = async ({ questRepository, eligibilityRepository, rewardRepository, - campaignParticipationRepository, }) => { const quests = await questRepository.findAll(); @@ -75,31 +13,20 @@ export const getQuestResultsForCampaignParticipation = async ({ return []; } - const eligibility = await getEligibilityForThisCampaignParticipation( - eligibilityRepository, - userId, - campaignParticipationId, - ); + const campaignParticipation = await campaignParticipationRepository.get(campaignParticipationId); - if (!eligibility) return []; - - const targetProfileIdsForThisCampaignParticipation = await getTargetProfilesForThisCampaignParticipation({ - campaignParticipationRepository, - campaignParticipationId, - quests, - eligibility, - }); + const eligibilities = await eligibilityRepository.find({ userId }); + 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 }), + ); + console.log({ questsRelatedToCampaignParticipation }); 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); } diff --git a/api/src/quest/domain/usecases/reward-user.js b/api/src/quest/domain/usecases/reward-user.js index a09aacf19e6..cf09b31a9e5 100644 --- a/api/src/quest/domain/usecases/reward-user.js +++ b/api/src/quest/domain/usecases/reward-user.js @@ -31,7 +31,10 @@ export const rewardUser = async ({ continue; } - const success = await successRepository.find({ userId, skillIds: quest.successRequirements[0].data.ids }); + const success = await successRepository.find({ + userId, + skillIds: quest.successRequirements[0].data.ids, + }); const userHasSucceedQuest = quest.isSuccessful(success); if (userHasSucceedQuest) { diff --git a/api/tests/prescription/organization-learner/unit/application/api/models/OrganizationLearnerWithParticipations_test.js b/api/tests/prescription/organization-learner/unit/application/api/models/OrganizationLearnerWithParticipations_test.js index 98debf61b8d..7856cfd0677 100644 --- a/api/tests/prescription/organization-learner/unit/application/api/models/OrganizationLearnerWithParticipations_test.js +++ b/api/tests/prescription/organization-learner/unit/application/api/models/OrganizationLearnerWithParticipations_test.js @@ -37,8 +37,16 @@ describe('Unit | Application| API | Models | OrganizationLearnerWithParticipatio type: organization.type, }); expect(organizationLearnerWithParticipations.campaignParticipations).to.deep.have.members([ - { targetProfileId: participationsList[0].targetProfileId, id: participationsList[0].id }, - { targetProfileId: participationsList[1].targetProfileId, id: participationsList[1].id }, + { + targetProfileId: participationsList[0].targetProfileId, + id: participationsList[0].id, + status: participationsList[0].status, + }, + { + targetProfileId: participationsList[1].targetProfileId, + id: participationsList[1].id, + status: participationsList[1].status, + }, ]); }); }); diff --git a/api/tests/prescription/organization-learner/unit/application/api/organization-learners-with-participations-api_test.js b/api/tests/prescription/organization-learner/unit/application/api/organization-learners-with-participations-api_test.js index a9dc567abeb..eee74ca074c 100644 --- a/api/tests/prescription/organization-learner/unit/application/api/organization-learners-with-participations-api_test.js +++ b/api/tests/prescription/organization-learner/unit/application/api/organization-learners-with-participations-api_test.js @@ -46,7 +46,11 @@ describe('Unit | API | Organization Learner With Participations', function () { tags: tagNames, type: organization1.type, }, - campaignParticipations: campaignParticipations.map(({ id, targetProfileId }) => ({ id, targetProfileId })), + campaignParticipations: campaignParticipations.map(({ id, targetProfileId, status }) => ({ + id, + targetProfileId, + status, + })), }, { organizationLearner: { @@ -59,7 +63,11 @@ describe('Unit | API | Organization Learner With Participations', function () { tags: tagNames, type: organization2.type, }, - campaignParticipations: campaignParticipations.map(({ id, targetProfileId }) => ({ id, targetProfileId })), + campaignParticipations: campaignParticipations.map(({ id, targetProfileId, status }) => ({ + id, + targetProfileId, + status, + })), }, ]); }); diff --git a/api/tests/quest/integration/domain/usecases/get-quest-results-for-campaign-participation_test.js b/api/tests/quest/integration/domain/usecases/get-quest-results-for-campaign-participation_test.js index ac240e9df3e..8245b33ab8c 100644 --- a/api/tests/quest/integration/domain/usecases/get-quest-results-for-campaign-participation_test.js +++ b/api/tests/quest/integration/domain/usecases/get-quest-results-for-campaign-participation_test.js @@ -3,7 +3,7 @@ import { QuestResult } from '../../../../../src/quest/domain/models/QuestResult. import { usecases } from '../../../../../src/quest/domain/usecases/index.js'; import { databaseBuilder, expect } from '../../../../test-helper.js'; -describe('Quest | Integration | Domain | Usecases | getQuestResultsForCampaignParticipation', function () { +describe.only('Quest | Integration | Domain | Usecases | getQuestResultsForCampaignParticipation', function () { describe('when there are multiple target profiles in the quest requirements', function () { it('should get quest results for campaign participation belonging to one of the target profiles', async function () { const organizationId = databaseBuilder.factory.buildOrganization({ type: 'SCO' }).id; diff --git a/api/tests/quest/integration/domain/usecases/reward-user_test.js b/api/tests/quest/integration/domain/usecases/reward-user_test.js index d551967dcc8..7d9418daafb 100644 --- a/api/tests/quest/integration/domain/usecases/reward-user_test.js +++ b/api/tests/quest/integration/domain/usecases/reward-user_test.js @@ -1,5 +1,7 @@ import { PROFILE_REWARDS_TABLE_NAME } from '../../../../../db/migrations/20240820101213_add-profile-rewards-table.js'; +import { TYPES } from '../../../../../src/quest/domain/models/Eligibility.js'; import { COMPARISON } from '../../../../../src/quest/domain/models/Quest.js'; +import { TYPES as SUCCESS_TYPES } from '../../../../../src/quest/domain/models/Success.js'; import { usecases } from '../../../../../src/quest/domain/usecases/index.js'; import { KnowledgeElement } from '../../../../../src/shared/domain/models/index.js'; import { databaseBuilder, expect, knex } from '../../../../test-helper.js'; @@ -54,7 +56,7 @@ const setupContext = async ( const quest = databaseBuilder.factory.buildQuest({ eligibilityRequirements: [ { - type: 'organization', + type: TYPES.ORGANIZATION, comparison: COMPARISON.ALL, data: { type: questOrganization, @@ -63,7 +65,7 @@ const setupContext = async ( ], successRequirements: [ { - type: 'skills', + type: SUCCESS_TYPES.SKILL, data: { ids: ['skillId1', 'skillId2', 'skillId3'], threshold: 50, @@ -152,7 +154,7 @@ describe('Quest | Integration | Domain | Usecases | RewardUser', function () { rewardId, eligibilityRequirements: [ { - type: 'organization', + type: TYPES.ORGANIZATION, comparison: COMPARISON.ALL, data: { type: questOrganization, @@ -161,7 +163,7 @@ describe('Quest | Integration | Domain | Usecases | RewardUser', function () { ], successRequirements: [ { - type: 'skills', + type: SUCCESS_TYPES.SKILL, data: { ids: ['skillId1', 'skillId2', 'skillId3'], threshold: 50, @@ -173,7 +175,7 @@ describe('Quest | Integration | Domain | Usecases | RewardUser', function () { rewardId, eligibilityRequirements: [ { - type: 'organization', + type: TYPES.ORGANIZATION, comparison: COMPARISON.ALL, data: { type: questOrganization, @@ -182,7 +184,7 @@ describe('Quest | Integration | Domain | Usecases | RewardUser', function () { ], successRequirements: [ { - type: 'skills', + type: SUCCESS_TYPES.SKILL, data: { ids: ['skillId1', 'skillId2', 'skillId3'], threshold: 50, diff --git a/api/tests/quest/unit/domain/models/Quest_test.js b/api/tests/quest/unit/domain/models/Quest_test.js index 54c6841e1cf..43d17583a1f 100644 --- a/api/tests/quest/unit/domain/models/Quest_test.js +++ b/api/tests/quest/unit/domain/models/Quest_test.js @@ -1,104 +1,83 @@ -import { Eligibility } from '../../../../../src/quest/domain/models/Eligibility.js'; +import { Eligibility, TYPES } from '../../../../../src/quest/domain/models/Eligibility.js'; import { Quest } from '../../../../../src/quest/domain/models/Quest.js'; import { COMPARISON } from '../../../../../src/quest/domain/models/Quest.js'; -import { Success } from '../../../../../src/quest/domain/models/Success.js'; -import { KnowledgeElement } from '../../../../../src/shared/domain/models/index.js'; -import { expect } from '../../../../test-helper.js'; +import { Success, TYPES as SUCCESS_TYPES } from '../../../../../src/quest/domain/models/Success.js'; +import { CampaignParticipationStatuses, KnowledgeElement } from '../../../../../src/shared/domain/models/index.js'; +import { domainBuilder, expect } from '../../../../test-helper.js'; describe('Quest | Unit | Domain | Models | Quest ', function () { describe('#isEligible', function () { - describe('when comparison is "all"', function () { - describe('when data to test is a simple value', function () { - let quest; - - before(function () { - // given - const eligibilityRequirements = [ + describe('edge cases', function () { + it('make it work with targetProfileIds', function () { + const quest = new Quest({ + eligibilityRequirements: [ { - type: 'organization', + type: TYPES.CAMPAIGN_PARTICIPATIONS, // Array data: { - type: 'SCO', + targetProfileIds: [1], }, comparison: COMPARISON.ALL, }, - ]; - quest = new Quest({ eligibilityRequirements }); + ], }); - it('should return true if is eligible', function () { - // when - const organization = { type: 'SCO' }; - const eligibilityData = new Eligibility({ organization }); - - // then - expect(quest.isEligible(eligibilityData)).to.equal(true); + const eligibilityData = new Eligibility({ + campaignParticipations: [{ targetProfileId: 2 }, { targetProfileId: 1 }], }); - it('should return false if is not eligible', function () { - // when - const organization = { type: 'PRO' }; - const eligibilityData = new Eligibility({ organization }); - - // then - expect(quest.isEligible(eligibilityData)).to.equal(false); - }); + expect(quest.isEligible(eligibilityData)).to.be.true; }); - describe('when data to test is an array', function () { - let quest; - - before(function () { - const eligibilityRequirements = [ + it('should return false when criteria are valid but not in the same object', function () { + const quest = new Quest({ + eligibilityRequirements: [ { - type: 'organization', + type: TYPES.CAMPAIGN_PARTICIPATIONS, // Array data: { - tags: ['AGRICULTURE', 'AEFE'], + targetProfileId: [1], + status: { + value: ['SHARED', 'TO_SHARE'], + comparison: COMPARISON.ONE_OF, + }, }, + comparison: COMPARISON.ALL, }, - ]; - quest = new Quest({ eligibilityRequirements }); + ], }); - it('should return true if is eligible', function () { - // when - const organization = { tags: ['MARITIME', 'AGRICULTURE', 'AEFE'] }; - const eligibilityData = new Eligibility({ organization }); - - // then - expect(quest.isEligible(eligibilityData)).to.equal(true); + const eligibilityData = new Eligibility({ + campaignParticipations: [ + { targetProfileId: 2, status: 'SHARED' }, + { targetProfileId: 1, status: 'STARTED' }, + ], }); - it('should return false if is not eligible', function () { - // when - const organization = { tags: ['MARITIME', 'AGRICULTURE'] }; - const eligibilityData = new Eligibility({ organization }); - - // then - expect(quest.isEligible(eligibilityData)).to.equal(false); - }); + expect(quest.isEligible(eligibilityData)).to.be.false; }); }); + }); - describe('when comparison is "one-of"', function () { + describe('when comparison is "all"', function () { + describe('when data to test is a simple value', function () { let quest; before(function () { + // given const eligibilityRequirements = [ { - type: 'organization', + type: TYPES.ORGANIZATION, data: { - isManagingStudents: true, - tags: ['AEFE'], + type: 'SCO', }, - comparison: 'one-of', + comparison: COMPARISON.ALL, }, ]; quest = new Quest({ eligibilityRequirements }); }); it('should return true if is eligible', function () { - //when - const organization = { isManagingStudents: true, tags: ['MARITIME'] }; + // when + const organization = { type: 'SCO' }; const eligibilityData = new Eligibility({ organization }); // then @@ -106,8 +85,8 @@ describe('Quest | Unit | Domain | Models | Quest ', function () { }); it('should return false if is not eligible', function () { - //when - const organization = { isManagingStudents: false, tags: ['MARITIME', 'AGRICULTURE'] }; + // when + const organization = { type: 'PRO' }; const eligibilityData = new Eligibility({ organization }); // then @@ -115,63 +94,33 @@ describe('Quest | Unit | Domain | Models | Quest ', function () { }); }); - describe('when there are multiple eligibility requirements', function () { + describe('when data to test is an array', function () { let quest; - let userTargetProfileId; before(function () { - // given - const eligibleTargetProfileId = 1000; - userTargetProfileId = eligibleTargetProfileId; - const eligibilityRequirements = [ { - type: 'organization', + type: TYPES.ORGANIZATION, data: { - type: 'SCO', + tags: ['AGRICULTURE', 'AEFE'], }, - comparison: COMPARISON.ALL, - }, - { - type: 'organization', - data: { - isManagingStudents: true, - tags: ['AEFE'], - }, - comparison: COMPARISON.ONE_OF, - }, - { - type: 'organizationLearner', - data: { - MEFCode: '10010012110', - }, - comparison: COMPARISON.ALL, - }, - { - type: 'campaignParticipations', - data: { - targetProfileIds: [eligibleTargetProfileId], - }, - comparison: COMPARISON.ALL, }, ]; quest = new Quest({ eligibilityRequirements }); }); - it('should return true if all eligibility requirements are met', function () { + it('should return true if is eligible', function () { // when - const organization = { type: 'SCO', isManagingStudents: true, tags: ['AEFE'] }; - const organizationLearner = { MEFCode: '10010012110' }; - const campaignParticipations = [{ targetProfileId: userTargetProfileId }, { targetProfileId: 2000 }]; - const eligibilityData = new Eligibility({ organization, organizationLearner, campaignParticipations }); + const organization = { tags: ['MARITIME', 'AGRICULTURE', 'AEFE'] }; + const eligibilityData = new Eligibility({ organization }); // then expect(quest.isEligible(eligibilityData)).to.equal(true); }); - it('should return false if only some eligibility requirements are met', function () { + it('should return false if is not eligible', function () { // when - const organization = { type: 'PRO', isManagingStudents: true, tags: ['AEFE'] }; + const organization = { tags: ['MARITIME', 'AGRICULTURE'] }; const eligibilityData = new Eligibility({ organization }); // then @@ -180,14 +129,177 @@ describe('Quest | Unit | Domain | Models | Quest ', function () { }); }); - describe('#isSuccessful', function () { + describe('when comparison is "one-of"', function () { + let quest; + + before(function () { + const eligibilityRequirements = [ + { + type: TYPES.ORGANIZATION, + data: { + isManagingStudents: true, + tags: ['AEFE'], + }, + comparison: 'one-of', + }, + ]; + quest = new Quest({ eligibilityRequirements }); + }); + + it('should return true if is eligible', function () { + //when + const organization = { isManagingStudents: true, tags: ['MARITIME'] }; + const eligibilityData = new Eligibility({ organization }); + + // then + expect(quest.isEligible(eligibilityData)).to.equal(true); + }); + + it('should return false if is not eligible', function () { + //when + const organization = { isManagingStudents: false, tags: ['MARITIME', 'AGRICULTURE'] }; + const eligibilityData = new Eligibility({ organization }); + + // then + expect(quest.isEligible(eligibilityData)).to.equal(false); + }); + }); + + describe('when there are multiple eligibility requirements', function () { + let quest; + let userTargetProfileId; + + before(function () { + // given + const eligibleTargetProfileId = 1000; + userTargetProfileId = eligibleTargetProfileId; + + const eligibilityRequirements = [ + { + type: TYPES.ORGANIZATION, + data: { + type: 'SCO', + }, + comparison: COMPARISON.ALL, + }, + { + type: TYPES.ORGANIZATION, + data: { + isManagingStudents: true, + tags: ['AEFE'], + }, + comparison: COMPARISON.ONE_OF, + }, + { + type: TYPES.ORGANIZATION_LEARNER, // Object + data: { + MEFCode: '10010012110', + }, + comparison: COMPARISON.ALL, + }, + { + type: TYPES.CAMPAIGN_PARTICIPATIONS, // Array + data: { + targetProfileId: [eligibleTargetProfileId], + }, + comparison: COMPARISON.ALL, + }, + ]; + quest = new Quest({ eligibilityRequirements }); + }); + + it('should return true if all eligibility requirements are met', function () { + // when + const organization = { type: 'SCO', isManagingStudents: true, tags: ['AEFE'] }; + const organizationLearner = { MEFCode: '10010012110' }; + const campaignParticipations = [{ targetProfileId: userTargetProfileId }, { targetProfileId: 2000 }]; + const eligibilityData = new Eligibility({ organization, organizationLearner, campaignParticipations }); + + // then + expect(quest.isEligible(eligibilityData)).to.equal(true); + }); + + it('should return false if only some eligibility requirements are met', function () { + // when + const organization = { type: 'PRO', isManagingStudents: true, tags: ['AEFE'] }; + const eligibilityData = new Eligibility({ organization }); + + // then + expect(quest.isEligible(eligibilityData)).to.equal(false); + }); + }); + + describe('when you want different comparison of data', function () { let quest; before(function () { // given - const successRequirements = [ + const eligibilityRequirements = [ + { + type: TYPES.CAMPAIGN_PARTICIPATIONS, + data: { + status: { + value: [CampaignParticipationStatuses.TO_SHARE, CampaignParticipationStatuses.SHARED], + comparison: COMPARISON.ONE_OF, + }, + }, + comparison: COMPARISON.ALL, + }, + ]; + quest = new Quest({ eligibilityRequirements }); + }); + + it('should return true if eligibility requirements are met', function () { + // when + const campaignParticipations = [ + { status: CampaignParticipationStatuses.TO_SHARE }, + { status: CampaignParticipationStatuses.STARTED }, + ]; + const eligibilityData = new Eligibility({ organizationLearner: {}, organization: {}, campaignParticipations }); + + // then + expect(quest.isEligible(eligibilityData)).to.equal(true); + }); + + it('should return false if eligibility requirements are met', function () { + // when + const eligibilityData = new Eligibility({ + organizationLearner: {}, + organization: {}, + campaignParticipations: [], + }); + + // then + expect(quest.isEligible(eligibilityData)).to.equal(false); + }); + }); +}); + +describe('#isSuccessful', function () { + describe('without requirement', function () { + it('should return true if successRequirement is undefined', function () { + const quest = new Quest({}); + + //then + expect(quest.isSuccessful()).to.equal(true); + }); + + it('should return true if successRequirement is empty array', function () { + const quest = new Quest({ successRequirements: [] }); + + //then + expect(quest.isSuccessful()).to.equal(true); + }); + }); + + describe('type of requirement is SKILL', function () { + let quest, successRequirements; + + before(function () { + // given + successRequirements = [ { - type: 'skill', + type: SUCCESS_TYPES.SKILL, data: { ids: [1, 2, 3], threshold: 50, @@ -201,10 +313,9 @@ describe('Quest | Unit | Domain | Models | Quest ', function () { // when const success = new Success({ knowledgeElements: [ - { status: KnowledgeElement.StatusType.VALIDATED }, - { status: KnowledgeElement.StatusType.VALIDATED }, - { status: KnowledgeElement.StatusType.VALIDATED }, - { status: KnowledgeElement.StatusType.INVALIDATED }, + domainBuilder.buildKnowledgeElement({ skillId: 1, status: KnowledgeElement.StatusType.VALIDATED }), + domainBuilder.buildKnowledgeElement({ skillId: 2, status: KnowledgeElement.StatusType.VALIDATED }), + domainBuilder.buildKnowledgeElement({ skillId: 3, status: KnowledgeElement.StatusType.INVALIDATED }), ], }); @@ -216,15 +327,68 @@ describe('Quest | Unit | Domain | Models | Quest ', function () { // when const success = new Success({ knowledgeElements: [ - { status: KnowledgeElement.StatusType.VALIDATED }, - { status: KnowledgeElement.StatusType.INVALIDATED }, - { status: KnowledgeElement.StatusType.INVALIDATED }, - { status: KnowledgeElement.StatusType.INVALIDATED }, + domainBuilder.buildKnowledgeElement({ skillId: 1, status: KnowledgeElement.StatusType.VALIDATED }), + domainBuilder.buildKnowledgeElement({ skillId: 2, status: KnowledgeElement.StatusType.INVALIDATED }), + domainBuilder.buildKnowledgeElement({ skillId: 3, status: KnowledgeElement.StatusType.INVALIDATED }), ], }); // then expect(quest.isSuccessful(success)).to.equal(false); }); + + it('should return true if multiple success requirements are met', function () { + // when + const success = new Success({ + knowledgeElements: [ + domainBuilder.buildKnowledgeElement({ skillId: 1, status: KnowledgeElement.StatusType.VALIDATED }), + domainBuilder.buildKnowledgeElement({ skillId: 2, status: KnowledgeElement.StatusType.VALIDATED }), + domainBuilder.buildKnowledgeElement({ skillId: 3, status: KnowledgeElement.StatusType.INVALIDATED }), + domainBuilder.buildKnowledgeElement({ skillId: 4, status: KnowledgeElement.StatusType.VALIDATED }), + domainBuilder.buildKnowledgeElement({ skillId: 5, status: KnowledgeElement.StatusType.VALIDATED }), + ], + }); + + successRequirements = [ + ...successRequirements, + { + type: SUCCESS_TYPES.SKILL, + data: { + ids: [4, 5], + threshold: 100, + }, + }, + ]; + quest = new Quest({ successRequirements }); + //then + expect(quest.isSuccessful(success)).to.equal(true); + }); + + it('should return false if multiple success requirements are met', function () { + // when + const success = new Success({ + knowledgeElements: [ + domainBuilder.buildKnowledgeElement({ skillId: 1, status: KnowledgeElement.StatusType.VALIDATED }), + domainBuilder.buildKnowledgeElement({ skillId: 2, status: KnowledgeElement.StatusType.VALIDATED }), + domainBuilder.buildKnowledgeElement({ skillId: 3, status: KnowledgeElement.StatusType.INVALIDATED }), + domainBuilder.buildKnowledgeElement({ skillId: 4, status: KnowledgeElement.StatusType.VALIDATED }), + domainBuilder.buildKnowledgeElement({ skillId: 5, status: KnowledgeElement.StatusType.INVALIDATED }), + ], + }); + + successRequirements = [ + ...successRequirements, + { + type: SUCCESS_TYPES.SKILL, + data: { + ids: [4, 5], + threshold: 100, + }, + }, + ]; + quest = new Quest({ successRequirements }); + //then + expect(quest.isSuccessful(success)).to.equal(false); + }); }); }); diff --git a/api/tests/quest/unit/domain/usecases/get-quest-results-for-campaign-participation_test.js b/api/tests/quest/unit/domain/usecases/get-quest-results-for-campaign-participation_test.js index afea35d67af..f6d22d54881 100644 --- a/api/tests/quest/unit/domain/usecases/get-quest-results-for-campaign-participation_test.js +++ b/api/tests/quest/unit/domain/usecases/get-quest-results-for-campaign-participation_test.js @@ -1,4 +1,4 @@ -import { Eligibility } from '../../../../../src/quest/domain/models/Eligibility.js'; +import { Eligibility, TYPES } from '../../../../../src/quest/domain/models/Eligibility.js'; import { Quest } from '../../../../../src/quest/domain/models/Quest.js'; import { getQuestResultsForCampaignParticipation } from '../../../../../src/quest/domain/usecases/get-quest-results-for-campaign-participation.js'; import { expect, sinon } from '../../../../test-helper.js'; @@ -74,7 +74,7 @@ describe('Quest | Unit | Domain | Usecases | getQuestResultsForCampaignParticipa new Quest({ id: 10, eligibilityRequirements: [ - { type: 'campaignParticipations', data: { targetProfileIds: [wrongTargetProfileId] } }, + { type: TYPES.CAMPAIGN_PARTICIPATIONS, data: { targetProfileIds: [wrongTargetProfileId] } }, ], successRequirements: [], rewardType: 'attestations',