Skip to content

Commit

Permalink
[FEATURE] Ecoute les évènements questions répondue et détermine si un…
Browse files Browse the repository at this point in the history
…e quête est validée (PIX-13819). (#10108)

Co-authored-by: Guillaume Olejniczak <[email protected]>
Co-authored-by: Quentin Lebouc <[email protected]>
  • Loading branch information
3 people authored Sep 23, 2024
1 parent 7141a64 commit 43927ad
Show file tree
Hide file tree
Showing 47 changed files with 1,218 additions and 28 deletions.
1 change: 1 addition & 0 deletions api/Procfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ postdeploy: npm run postdeploy
# for more information
web: exec node index.js
worker: exec node worker.js
fastworker: exec node worker.js fast
3 changes: 2 additions & 1 deletion api/db/database-builder/factory/build-attestation.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ATTESTATIONS_TABLE_NAME } from '../../migrations/20240820101115_add-attestations-table.js';
import { databaseBuffer } from '../database-buffer.js';

const buildAttestation = function ({
Expand All @@ -12,7 +13,7 @@ const buildAttestation = function ({
};

return databaseBuffer.pushInsertable({
tableName: 'attestations',
tableName: ATTESTATIONS_TABLE_NAME,
values,
});
};
Expand Down
8 changes: 5 additions & 3 deletions api/db/database-builder/factory/build-profile-reward.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import isUndefined from 'lodash/isUndefined.js';

import { REWARD_TYPES } from '../../../src/quest/domain/constants.js';
import { PROFILE_REWARDS_TABLE_NAME } from '../../migrations/20240820101213_add-profile-rewards-table.js';
import { databaseBuffer } from '../database-buffer.js';
import { buildAttestation } from './build-attestation.js';
import { buildUser } from './build-user.js';

const buildProfileReward = function ({
id = databaseBuffer.getNextId(),
createdAt = new Date(),
rewardType = 'attestations',
rewardType = REWARD_TYPES.ATTESTATION,
rewardId,
userId,
} = {}) {
userId = isUndefined(userId) ? buildUser().id : userId;
rewardId = isUndefined(rewardId) && rewardType === 'attestations' ? buildAttestation().id : rewardId;
rewardId = isUndefined(rewardId) && rewardType === REWARD_TYPES.ATTESTATION ? buildAttestation().id : rewardId;

const values = {
id,
Expand All @@ -23,7 +25,7 @@ const buildProfileReward = function ({
};

return databaseBuffer.pushInsertable({
tableName: 'profile-rewards',
tableName: PROFILE_REWARDS_TABLE_NAME,
values,
});
};
Expand Down
5 changes: 3 additions & 2 deletions api/db/database-builder/factory/build-quest.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import isUndefined from 'lodash/isUndefined.js';

import { REWARD_TYPES } from '../../../src/quest/domain/constants.js';
import { databaseBuffer } from '../database-buffer.js';
import { buildAttestation } from './build-attestation.js';

const buildQuest = function ({
id = databaseBuffer.getNextId(),
createdAt = new Date(),
rewardType = 'attestations',
rewardType = REWARD_TYPES.ATTESTATION,
rewardId,
eligibilityRequirements,
successRequirements,
} = {}) {
rewardId = isUndefined(rewardId) && rewardType === 'attestations' ? buildAttestation().id : rewardId;
rewardId = isUndefined(rewardId) && rewardType === REWARD_TYPES.ATTESTATION ? buildAttestation().id : rewardId;
eligibilityRequirements = JSON.stringify(eligibilityRequirements);
successRequirements = JSON.stringify(successRequirements);

Expand Down
6 changes: 3 additions & 3 deletions api/db/migrations/20240820101115_add-attestations-table.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@
// If your migrations target `answers` or `knowledge-elements`
// contact @team-captains, because automatic migrations are not active on `pix-datawarehouse-production`
// this may prevent data replication to succeed the day after your migration is deployed on `pix-api-production`
const TABLE_NAME = 'attestations';
export const ATTESTATIONS_TABLE_NAME = 'attestations';

const up = async function (knex) {
await knex.schema.createTable(TABLE_NAME, function (table) {
await knex.schema.createTable(ATTESTATIONS_TABLE_NAME, function (table) {
table.increments('id').primary();
table.dateTime('createdAt').notNullable().defaultTo(knex.fn.now());
table.string('templateName').notNullable();
});
};

const down = async function (knex) {
return knex.schema.dropTable(TABLE_NAME);
return knex.schema.dropTable(ATTESTATIONS_TABLE_NAME);
};

export { down, up };
11 changes: 6 additions & 5 deletions api/db/migrations/20240820101213_add-profile-rewards-table.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,21 @@
// If your migrations target `answers` or `knowledge-elements`
// contact @team-captains, because automatic migrations are not active on `pix-datawarehouse-production`
// this may prevent data replication to succeed the day after your migration is deployed on `pix-api-production`
const TABLE_NAME = 'profile-rewards';
import { REWARD_TYPES } from '../../src/quest/domain/constants.js';
export const PROFILE_REWARDS_TABLE_NAME = 'profile-rewards';

const up = async function (knex) {
await knex.schema.createTable(TABLE_NAME, function (table) {
await knex.schema.createTable(PROFILE_REWARDS_TABLE_NAME, function (table) {
table.increments('id').primary();
table.dateTime('createdAt').notNullable().defaultTo(knex.fn.now());
table.bigInteger('userId').index().references('users.id');
table.string('rewardType').notNullable();
table.string('rewardId').notNullable();
table.enum('rewardType', [REWARD_TYPES.ATTESTATION]).defaultTo(REWARD_TYPES.ATTESTATION);
table.bigInteger('rewardId').notNullable();
});
};

const down = async function (knex) {
return knex.schema.dropTable(TABLE_NAME);
return knex.schema.dropTable(PROFILE_REWARDS_TABLE_NAME);
};

export { down, up };
13 changes: 9 additions & 4 deletions api/db/seeds/data/team-prescription/build-quests.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { REWARD_TYPES } from '../../../../src/quest/domain/constants.js';
import { COMPARISON } from '../../../../src/quest/domain/models/Quest.js';
import { TARGET_PROFILE_BADGES_STAGES_ID } from './constants.js';

async function createAttestationQuest(databasebuilder) {
Expand All @@ -15,26 +17,29 @@ async function createAttestationQuest(databasebuilder) {
data: {
type: 'SCO',
},
comparison: COMPARISON.ALL,
},
{
type: 'organization',
data: {
isManagingStudents: true,
tagNames: ['AEFE'],
tags: ['AEFE'],
},
comparison: 'one-of',
comparison: COMPARISON.ONE_OF,
},
{
type: 'organizationLearner',
data: {
MEFCode: '10010012110',
},
comparison: COMPARISON.ALL,
},
{
type: 'campaignParticipations',
data: {
targetProfileIds: [TARGET_PROFILE_BADGES_STAGES_ID],
},
comparison: COMPARISON.ALL,
},
];
const questSuccessRequirements = [
Expand All @@ -48,7 +53,7 @@ async function createAttestationQuest(databasebuilder) {
];

await databasebuilder.factory.buildQuest({
rewardType: 'attestation',
rewardType: REWARD_TYPES.ATTESTATION,
rewardId,
eligibilityRequirements: questEligibilityRequirements,
successRequirements: questSuccessRequirements,
Expand All @@ -58,7 +63,7 @@ async function createAttestationQuest(databasebuilder) {
successfulUsers.map(({ userId }) => {
return databasebuilder.factory.buildProfileReward({
userId,
rewardType: 'attestation',
rewardType: REWARD_TYPES.ATTESTATION,
rewardId,
});
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,21 @@ function _applyFilters(knowledgeElements) {
return _dropResetKnowledgeElements(uniqsMostRecentPerSkill);
}

function _findByUserIdAndLimitDateQuery({ userId, limitDate }) {
function _findByUserIdAndLimitDateQuery({ userId, limitDate, skillIds = [] }) {
const knexConn = DomainTransaction.getConnection();
return knexConn(tableName).where((qb) => {
qb.where({ userId });
if (limitDate) {
qb.where('createdAt', '<', limitDate);
}
if (skillIds.length) {
qb.whereIn('skillId', skillIds);
}
});
}

async function _findAssessedByUserIdAndLimitDateQuery({ userId, limitDate }) {
const knowledgeElementRows = await _findByUserIdAndLimitDateQuery({ userId, limitDate });
async function _findAssessedByUserIdAndLimitDateQuery({ userId, limitDate, skillIds }) {
const knowledgeElementRows = await _findByUserIdAndLimitDateQuery({ userId, limitDate, skillIds });

const knowledgeElements = _.map(
knowledgeElementRows,
Expand Down Expand Up @@ -84,8 +87,8 @@ const batchSave = async function ({ knowledgeElements }) {
return savedKnowledgeElements.map((ke) => new KnowledgeElement(ke));
};

const findUniqByUserId = function ({ userId, limitDate }) {
return _findAssessedByUserIdAndLimitDateQuery({ userId, limitDate });
const findUniqByUserId = function ({ userId, limitDate, skillIds }) {
return _findAssessedByUserIdAndLimitDateQuery({ userId, limitDate, skillIds });
};

const findUniqByUserIdAndAssessmentId = async function ({ userId, assessmentId }) {
Expand Down
2 changes: 2 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,9 @@
"start": "node index.js",
"start:watch": "npm run dev",
"start:job": "node worker.js",
"start:job:fast": "node worker.js fast",
"start:job:watch": "nodemon worker.js",
"start:job:fast:watch": "nodemon worker.js fast",
"test": "NODE_ENV=test npm run db:prepare && npm run test:api",
"test:api": "for testType in 'unit' 'integration' 'acceptance'; do npm run test:api:$testType || status=1 ; done ; exit $status",
"test:api:path": "NODE_ENV=test mocha --exit --recursive --reporter=${MOCHA_REPORTER:-dot}",
Expand Down
33 changes: 33 additions & 0 deletions api/src/evaluation/application/api/knowledge-elements-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { evaluationUsecases } from '../../domain/usecases/index.js';
import { KnowledgeElementDTO } from './models/KnowledgeElementDTO.js';

/**
* @typedef KnowledgeElementDTO
* @type {object}
* @property {string} status
*/

/**
* @typedef Payload
* @type {object}
* @property {number} userId
* @property {Array<string>} skillIds
*/

/**
* @function
* @name findFilteredMostRecentByUser
*
* @param {Payload} payload
* @returns {Promise<Array<KnowledgeElementDTO>>}
*/
export async function findFilteredMostRecentByUser({ userId, skillIds }) {
const knowledgeElements = await evaluationUsecases.findFilteredMostRecentKnowledgeElementsByUser({
userId,
skillIds,
});

return knowledgeElements.map(_toApi);
}

const _toApi = (knowledgeElement) => new KnowledgeElementDTO(knowledgeElement);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class KnowledgeElementDTO {
constructor({ status }) {
this.status = status;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const findFilteredMostRecentKnowledgeElementsByUser = async ({
userId,
skillIds = [],
knowledgeElementRepository,
} = {}) => knowledgeElementRepository.findUniqByUserId({ userId, skillIds });

export { findFilteredMostRecentKnowledgeElementsByUser };
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { AnswerJob } from '../../../quest/domain/models/AnwserJob.js';
import { JobRepository } from '../../../shared/infrastructure/repositories/jobs/job-repository.js';

class AnswerJobRepository extends JobRepository {
constructor() {
super({
name: AnswerJob.name,
});
}
}

export const answerJobRepository = new AnswerJobRepository();
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const findOrganizationLearnersWithParticipations = withTransaction(async functio
return {
organizationLearner,
organization,
participations: campaignParticipationOverviews,
campaignParticipations: campaignParticipationOverviews,
tagNames: tags.map((tag) => tag.name),
};
}),
Expand Down
9 changes: 9 additions & 0 deletions api/src/profile/application/api/profile-reward-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { usecases } from '../../domain/usecases/index.js';

export const save = async (userId, rewardId) => {
return usecases.rewardUser({ userId, rewardId });
};

export const getByUserId = async (userId) => {
return usecases.getProfileRewardsByUserId({ userId });
};
7 changes: 7 additions & 0 deletions api/src/profile/domain/models/ProfileReward.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class ProfileReward {
constructor({ id, rewardId, rewardType } = {}) {
this.id = id;
this.rewardId = rewardId;
this.rewardType = rewardType;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const getProfileRewardsByUserId = async function ({ userId, profileRewardRepository }) {
return profileRewardRepository.getByUserId({ userId });
};
20 changes: 20 additions & 0 deletions api/src/profile/domain/usecases/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

import * as profileRewardRepository from '../../../profile/infrastructure/repositories/profile-reward-repository.js';
import { injectDependencies } from '../../../shared/infrastructure/utils/dependency-injection.js';
import { importNamedExportsFromDirectory } from '../../../shared/infrastructure/utils/import-named-exports-from-directory.js';

const path = dirname(fileURLToPath(import.meta.url));

const usecasesWithoutInjectedDependencies = {
...(await importNamedExportsFromDirectory({ path: join(path, './'), ignoredFileNames: ['index.js'] })),
};

const dependencies = {
profileRewardRepository,
};

const usecases = injectDependencies(usecasesWithoutInjectedDependencies, dependencies);

export { usecases };
3 changes: 3 additions & 0 deletions api/src/profile/domain/usecases/reward-user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const rewardUser = async function ({ userId, rewardId, profileRewardRepository }) {
return profileRewardRepository.save({ userId, rewardId });
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { PROFILE_REWARDS_TABLE_NAME } from '../../../../db/migrations/20240820101213_add-profile-rewards-table.js';
import { REWARD_TYPES } from '../../../quest/domain/constants.js';
import { DomainTransaction } from '../../../shared/domain/DomainTransaction.js';
import { ProfileReward } from '../../domain/models/ProfileReward.js';

/**
* @param {number} userId
* @param {number} rewardId
* @param {('ATTESTATION')} rewardType
* @returns {Promise<*>}
*/
export const save = async ({ userId, rewardId, rewardType = REWARD_TYPES.ATTESTATION }) => {
const knexConnection = await DomainTransaction.getConnection();
await knexConnection(PROFILE_REWARDS_TABLE_NAME).insert({
userId,
rewardId,
rewardType,
});
};

/**
* @param {number} userId
* @returns {Promise<*>}
*/
export const getByUserId = async ({ userId }) => {
const knexConnection = await DomainTransaction.getConnection();
const profileRewards = await knexConnection(PROFILE_REWARDS_TABLE_NAME).where({ userId });
return profileRewards.map(toDomain);
};

const toDomain = (profileReward) => {
return new ProfileReward(profileReward);
};
15 changes: 15 additions & 0 deletions api/src/quest/application/jobs/answer-job-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { JobController, JobGroup } from '../../../shared/application/jobs/job-controller.js';
import { AnswerJob } from '../../domain/models/AnwserJob.js';
import { usecases } from '../../domain/usecases/index.js';

export class AnswerJobController extends JobController {
constructor() {
super(AnswerJob.name, { jobGroup: JobGroup.FAST });
}

async handle({ data }) {
const { userId } = data;

return usecases.rewardUser({ userId });
}
}
3 changes: 3 additions & 0 deletions api/src/quest/domain/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { ATTESTATIONS_TABLE_NAME } from '../../../db/migrations/20240820101115_add-attestations-table.js';

export const REWARD_TYPES = { ATTESTATION: ATTESTATIONS_TABLE_NAME };
Loading

0 comments on commit 43927ad

Please sign in to comment.