Skip to content

Commit

Permalink
[FEATURE] Empecher l'affichage des resultats d'une attestation si ell…
Browse files Browse the repository at this point in the history
…e est liee a plusieurs profils cible (PIX-13828)

 #10659
  • Loading branch information
pix-service-auto-merge authored Nov 29, 2024
2 parents 53395bb + 5efdcbf commit ba1f0a8
Show file tree
Hide file tree
Showing 10 changed files with 503 additions and 163 deletions.
358 changes: 205 additions & 153 deletions api/db/seeds/data/team-prescription/build-quests.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion api/src/profile/domain/models/Campaign.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export class Campaign {
constructor({ id, organizationId }) {
constructor({ id, organizationId, targetProfileId }) {
this.id = id;
this.organizationId = organizationId;
this.targetProfileId = targetProfileId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Campaign } from '../../domain/models/Campaign.js';
export async function getCampaignByParticipationId({ campaignParticipationId }) {
const knexConnection = DomainTransaction.getConnection();
const campaign = await knexConnection('campaign-participations')
.select('campaigns.id', 'campaigns.organizationId')
.select('campaigns.id', 'campaigns.organizationId', 'campaigns.targetProfileId')
.innerJoin('campaigns', 'campaigns.id', 'campaign-participations.campaignId')
.where({ 'campaign-participations.id': campaignParticipationId })
.first();
Expand Down
12 changes: 12 additions & 0 deletions api/src/quest/domain/models/Eligibility.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export class Eligibility {
};
}

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

hasCampaignParticipation(campaignParticipationId) {
return Boolean(
this.#campaignParticipations.find(
Expand All @@ -23,6 +27,14 @@ export class Eligibility {
);
}

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

getTargetProfileForCampaignParticipation(campaignParticipationId) {
const campaignParticipation = this.#campaignParticipations.find(
(campaignParticipation) => campaignParticipation.id === campaignParticipationId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,99 @@
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,
]
);
};

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

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

const eligibilities = await eligibilityRepository.find({ userId });
const eligibility = eligibilities.find((e) => e.hasCampaignParticipation(campaignParticipationId));
const eligibility = await getEligibilityForThisCampaignParticipation(
eligibilityRepository,
userId,
campaignParticipationId,
);

if (!eligibility) return [];

/*
This effectively overrides the existing campaignParticipations property with a new getter that always returns the updated targetProfileIds array based on the provided campaignParticipationId.
We can't just reassign the getter with the new value, because the getter will still be called and the new value would be ignored
*/
Object.defineProperty(eligibility, 'campaignParticipations', {
get: () => ({ targetProfileIds: [eligibility.getTargetProfileForCampaignParticipation(campaignParticipationId)] }),
const targetProfileIdsForThisCampaignParticipation = await getTargetProfilesForThisCampaignParticipation({
campaignParticipationRepository,
campaignParticipationId,
quests,
eligibility,
});

eligibility.campaignParticipations = targetProfileIdsForThisCampaignParticipation.map((targetProfileId) => ({
targetProfileId,
}));

const questResults = [];
for (const quest of quests) {
const isEligibleForQuest = quest.isEligible(eligibility);
Expand Down
2 changes: 2 additions & 0 deletions api/src/quest/domain/usecases/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

import * as campaignParticipationRepository from '../../../profile/infrastructure/repositories/campaign-participation-repository.js';
import { injectDependencies } from '../../../shared/infrastructure/utils/dependency-injection.js';
import { importNamedExportsFromDirectory } from '../../../shared/infrastructure/utils/import-named-exports-from-directory.js';
import { repositories } from '../../infrastructure/repositories/index.js';
Expand All @@ -17,6 +18,7 @@ const dependencies = {
rewardRepository: repositories.rewardRepository,
successRepository: repositories.successRepository,
questRepository,
campaignParticipationRepository,
};

const usecases = injectDependencies(usecasesWithoutInjectedDependencies, dependencies);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe('Profile | Integration | Infrastructure | Repository | campaign-partici
expect(result).to.be.an.instanceOf(Campaign);
expect(result.id).to.equal(campaign.id);
expect(result.organizationId).to.equal(campaign.organizationId);
expect(result.targetProfileId).to.equal(campaign.targetProfileId);
});

it('return null if campaignParticipation does not exist', async function () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,167 @@ import { usecases } from '../../../../../src/quest/domain/usecases/index.js';
import { databaseBuilder, expect } from '../../../../test-helper.js';

describe('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;
const { id: organizationLearnerId, userId } = databaseBuilder.factory.buildOrganizationLearner({
organizationId,
});

// build target profiles

const firstTargetProfile = databaseBuilder.factory.buildTargetProfile({
ownerOrganizationId: organizationId,
});
const secondTargetProfile = databaseBuilder.factory.buildTargetProfile({
ownerOrganizationId: organizationId,
});

// build campaigns

const firstCampaign = databaseBuilder.factory.buildCampaign({
organizationId,
targetProfileId: firstTargetProfile.id,
});

const secondCampaign = databaseBuilder.factory.buildCampaign({
organizationId,
targetProfileId: secondTargetProfile.id,
});

// build campaign participations

databaseBuilder.factory.buildCampaignParticipation({
organizationLearnerId,
campaignId: firstCampaign.id,
userId,
});

const { id: secondCampaignParticipationId } = databaseBuilder.factory.buildCampaignParticipation({
organizationLearnerId,
campaignId: secondCampaign.id,
userId,
});

const rewardId = databaseBuilder.factory.buildAttestation().id;
const questId = databaseBuilder.factory.buildQuest({
rewardType: 'attestations',
rewardId,
eligibilityRequirements: [
{
type: 'organization',
data: {
type: 'SCO',
},
comparison: COMPARISON.ALL,
},
{
type: 'campaignParticipations',
data: {
targetProfileIds: [firstTargetProfile.id, secondTargetProfile.id],
},
comparison: COMPARISON.ALL,
},
],
successRequirements: [],
}).id;

await databaseBuilder.commit();

const result = await usecases.getQuestResultsForCampaignParticipation({
userId,
campaignParticipationId: secondCampaignParticipationId,
});

expect(result[0]).to.be.instanceOf(QuestResult);
expect(result[0].id).to.equal(questId);
expect(result[0].reward.id).to.equal(rewardId);
});

it('should not return quest results for campaign participation if user has not participated to campaigns linked to all profiles target in quest requirement', async function () {
const organizationId = databaseBuilder.factory.buildOrganization({ type: 'SCO' }).id;
const { id: organizationLearnerId, userId } = databaseBuilder.factory.buildOrganizationLearner({
organizationId,
});

// build target profiles

const firstTargetProfile = databaseBuilder.factory.buildTargetProfile({
ownerOrganizationId: organizationId,
});
const secondTargetProfile = databaseBuilder.factory.buildTargetProfile({
ownerOrganizationId: organizationId,
});
const thirdTargetProfile = databaseBuilder.factory.buildTargetProfile({
ownerOrganizationId: organizationId,
});

// build campaigns

const firstCampaign = databaseBuilder.factory.buildCampaign({
organizationId,
targetProfileId: firstTargetProfile.id,
});

const secondCampaign = databaseBuilder.factory.buildCampaign({
organizationId,
targetProfileId: secondTargetProfile.id,
});

databaseBuilder.factory.buildCampaign({
organizationId,
targetProfileId: thirdTargetProfile.id,
});

// build campaign participations

databaseBuilder.factory.buildCampaignParticipation({
organizationLearnerId,
campaignId: firstCampaign.id,
userId,
});

const { id: secondCampaignParticipationId } = databaseBuilder.factory.buildCampaignParticipation({
organizationLearnerId,
campaignId: secondCampaign.id,
userId,
});

const rewardId = databaseBuilder.factory.buildAttestation().id;

databaseBuilder.factory.buildQuest({
rewardType: 'attestations',
rewardId,
eligibilityRequirements: [
{
type: 'organization',
data: {
type: 'SCO',
},
comparison: COMPARISON.ALL,
},
{
type: 'campaignParticipations',
data: {
targetProfileIds: [firstTargetProfile.id, secondTargetProfile.id, thirdTargetProfile.id],
},
comparison: COMPARISON.ALL,
},
],
successRequirements: [],
}).id;

await databaseBuilder.commit();

const result = await usecases.getQuestResultsForCampaignParticipation({
userId,
campaignParticipationId: secondCampaignParticipationId,
});

expect(result).to.be.empty;
});
});

it('should get quest results for campaign participation', async function () {
const organizationId = databaseBuilder.factory.buildOrganization({ type: 'SCO' }).id;
const { id: organizationLearnerId, userId } = databaseBuilder.factory.buildOrganizationLearner({ organizationId });
Expand Down Expand Up @@ -33,6 +194,7 @@ describe('Quest | Integration | Domain | Usecases | getQuestResultsForCampaignPa

expect(result[0]).to.be.instanceOf(QuestResult);
expect(result[0].id).to.equal(questId);
expect(result[0].obtained).to.equal(false);
expect(result[0].reward.id).to.equal(rewardId);
});

Expand Down
32 changes: 32 additions & 0 deletions api/tests/quest/unit/domain/models/Eligibility_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,36 @@ describe('Quest | Unit | Domain | Models | Eligibility ', function () {
expect(result).to.be.null;
});
});

describe('#hasCampaignParticipationForTargetProfileId', function () {
it('should return true if has campaign participation for given target profile', function () {
// given
const campaignParticipations = [
{ id: 1, targetProfileId: 10 },
{ id: 2, targetProfileId: 20 },
];
const eligiblity = new Eligibility({ campaignParticipations });

// when
const result = eligiblity.hasCampaignParticipationForTargetProfileId(10);

// then
expect(result).to.be.true;
});

it('should return false if there are no campaign participation for given target profile', function () {
// given
const campaignParticipations = [
{ id: 1, targetProfileId: 10 },
{ id: 2, targetProfileId: 20 },
];
const eligiblity = new Eligibility({ campaignParticipations });

// when
const result = eligiblity.hasCampaignParticipationForTargetProfileId(1);

// then
expect(result).to.be.false;
});
});
});
Loading

0 comments on commit ba1f0a8

Please sign in to comment.