Skip to content

Commit

Permalink
tech(api): extract Criterion logic into a dedicated model
Browse files Browse the repository at this point in the history
Co-authored-by: Yvonnick Frin <[email protected]>
Co-authored-by: Xavier Carron <[email protected]>
  • Loading branch information
3 people committed Feb 10, 2025
1 parent 5a9118d commit 9ef0271
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 38 deletions.
36 changes: 36 additions & 0 deletions api/src/quest/domain/models/Criterion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export class Criterion {
#data;

constructor({ data }) {
this.#data = data;
}

get data() {
return Object.freeze(this.#data);
}

toDTO() {
return this.#data;
}

check({ item, comparisonFunction }) {
return Object.keys(this.#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.#checkCriterionAttribute({
criterionAttr: this.#data[key],
dataAttr: item[alterKey],
});
});
}

#checkCriterionAttribute({ criterionAttr, dataAttr }) {
if (Array.isArray(criterionAttr)) {
if (Array.isArray(dataAttr)) {
return criterionAttr.every((valueToTest) => dataAttr.includes(valueToTest));
}
return criterionAttr.some((valueToTest) => valueToTest === dataAttr);
}
return dataAttr === criterionAttr;
}
}
52 changes: 21 additions & 31 deletions api/src/quest/domain/models/EligibilityRequirement.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { Criterion } from './Criterion.js';
import { COMPARISON } from './Quest.js';

export const COMPOSE_TYPE = 'compose';

export class EligibilityRequirement {
#requirement_type;
#data;
#subRequirements = null;
#criterion = null;
#comparison;

constructor({ requirement_type, data, comparison }) {
this.#requirement_type = requirement_type;
if (this.#requirement_type === COMPOSE_TYPE) {
this.#data = data.map((eligibilityRequirementRaw) => new EligibilityRequirement(eligibilityRequirementRaw));
this.#subRequirements = data.map((eligibilityRequirement) => {
if (eligibilityRequirement instanceof EligibilityRequirement) {
return eligibilityRequirement;
}
return new EligibilityRequirement(eligibilityRequirement);
});
} else {
this.#data = data;
this.#criterion = data instanceof Criterion ? data : new Criterion({ data });
}
this.#comparison = comparison;
}
Expand All @@ -22,7 +29,10 @@ export class EligibilityRequirement {
}

get data() {
return Object.freeze(this.#data);
if (this.#requirement_type === COMPOSE_TYPE) {
return Object.freeze(this.#subRequirements);
}
return this.#criterion;
}

get comparison() {
Expand All @@ -32,17 +42,19 @@ export class EligibilityRequirement {
isEligible(eligibility) {
const comparisonFunction = this.#getComparisonFunction(this.#comparison);
if (this.#requirement_type === COMPOSE_TYPE) {
return this.#data[comparisonFunction]((eligibilityRequirement) => eligibilityRequirement.isEligible(eligibility));
return this.#subRequirements[comparisonFunction]((eligibilityRequirement) =>
eligibilityRequirement.isEligible(eligibility),
);
}
return this.#checkRequirement(eligibility);
}

toDTO() {
let data;
if (this.#requirement_type === COMPOSE_TYPE) {
data = this.#data.map((item) => item.toDTO());
data = this.#subRequirements.map((item) => item.toDTO());
} else {
data = this.#data;
data = this.#criterion.toDTO();
}
return {
requirement_type: this.#requirement_type,
Expand All @@ -60,32 +72,10 @@ export class EligibilityRequirement {

if (Array.isArray(eligibility[this.#requirement_type])) {
return eligibility[this.#requirement_type].some((item) => {
return Object.keys(this.#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: this.#data[key],
eligibilityData: item[alterKey],
});
});
return this.#criterion.check({ item, comparisonFunction });
});
}

return Object.keys(this.#data)[comparisonFunction]((key) => {
return this.#checkCriterion({
criterion: this.#data[key],
eligibilityData: eligibility[this.#requirement_type][key],
});
});
}

#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;
return this.#criterion.check({ item: eligibility[this.#requirement_type], comparisonFunction });
}
}
16 changes: 9 additions & 7 deletions api/src/quest/domain/models/Quest.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,13 @@ class Quest {

const eligibilityRequirements = otherRequirements;
if (campaignParticipationRequirements.length > 0) {
eligibilityRequirements.push({
requirement_type: COMPOSE_TYPE,
data: campaignParticipationRequirements,
comparison: COMPARISON.ONE_OF,
});
eligibilityRequirements.push(
new EligibilityRequirement({
requirement_type: COMPOSE_TYPE,
data: campaignParticipationRequirements,
comparison: COMPARISON.ONE_OF,
}),
);
}
const scopedEligibilityRequirements = new EligibilityRequirement({
requirement_type: COMPOSE_TYPE,
Expand Down Expand Up @@ -107,10 +109,10 @@ class Quest {

#flattenRequirementsByType(requirements, type) {
let result = [];
const filteredRequierements = requirements.filter((requirement) =>
const filteredRequirements = requirements.filter((requirement) =>
[type, COMPOSE_TYPE].includes(requirement.requirement_type),
);
for (const requirement of filteredRequierements) {
for (const requirement of filteredRequirements) {
if (requirement.requirement_type === COMPOSE_TYPE) {
result = result.concat(this.#flattenRequirementsByType(requirement.data, type));
} else {
Expand Down
19 changes: 19 additions & 0 deletions api/tests/quest/unit/domain/models/Criterion_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Criterion } from '../../../../../src/quest/domain/models/Criterion.js';
import { expect } from '../../../../test-helper.js';

describe('Quest | Unit | Domain | Models | Criterion ', function () {
describe('#toDTO', function () {
it('should return a DTO version of the criterion', function () {
// given
const criterion = new Criterion({
data: { some: 'awesome', cool: 'stuff' },
});

// when
const DTO = criterion.toDTO();

// then
expect(DTO).to.deep.equal({ some: 'awesome', cool: 'stuff' });
});
});
});
56 changes: 56 additions & 0 deletions api/tests/quest/unit/domain/models/EligibilityRequirement_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,62 @@ import { COMPARISON } from '../../../../../src/quest/domain/models/Quest.js';
import { expect } from '../../../../test-helper.js';

describe('Quest | Unit | Domain | Models | EligibilityRequirement ', function () {
describe('#constructor', function () {
context('when requirement_type is compose', function () {
it('should build the same instance whether we are passing through raw requirements or instanciated requirements', function () {
// given
const eligibilityRequirementA = new EligibilityRequirement({
requirement_type: COMPOSE_TYPE,
comparison: COMPARISON.ALL,
data: [
{
requirement_type: TYPES.CAMPAIGN_PARTICIPATIONS,
data: {
targetProfileId: 1,
},
comparison: COMPARISON.ONE_OF,
},
{
requirement_type: TYPES.CAMPAIGN_PARTICIPATIONS,
data: {
targetProfileId: 2,
},
comparison: COMPARISON.ALL,
},
],
});
const eligibilityRequirementB = new EligibilityRequirement({
requirement_type: eligibilityRequirementA.requirement_type,
comparison: eligibilityRequirementA.comparison,
data: eligibilityRequirementA.data,
});

// when / then
expect(eligibilityRequirementA.toDTO()).to.deep.equal(eligibilityRequirementB.toDTO());
});
});
context('when requirement_type is not compose', function () {
it('should build the same instance whether we are passing through raw criterion or instanciated criterion', function () {
// given
const eligibilityRequirementA = new EligibilityRequirement({
requirement_type: TYPES.CAMPAIGN_PARTICIPATIONS,
comparison: COMPARISON.ALL,
data: {
targetProfileId: 1,
},
});
const eligibilityRequirementB = new EligibilityRequirement({
requirement_type: eligibilityRequirementA.requirement_type,
comparison: eligibilityRequirementA.comparison,
data: eligibilityRequirementA.data,
});

// when / then
expect(eligibilityRequirementA.toDTO()).to.deep.equal(eligibilityRequirementB.toDTO());
});
});
});

describe('#toDTO', function () {
it('should recursively unstack inside EligibilityRequirements into DTO', function () {
// given
Expand Down

0 comments on commit 9ef0271

Please sign in to comment.