Skip to content

Commit

Permalink
[FEATURE] Ajoute une API interne pour générer un zip avec les attesta…
Browse files Browse the repository at this point in the history
…tions d'une liste d'utilisateurs (PIX-13822).

 #10232
  • Loading branch information
pix-service-auto-merge authored Oct 7, 2024
2 parents b4b440b + 62f502d commit 4ce3ee0
Show file tree
Hide file tree
Showing 18 changed files with 370 additions and 9 deletions.
3 changes: 3 additions & 0 deletions api/db/database-builder/factory/build-attestation.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { ATTESTATIONS } from '../../../src/profile/domain/constants.js';
import { ATTESTATIONS_TABLE_NAME } from '../../migrations/20240820101115_add-attestations-table.js';
import { databaseBuffer } from '../database-buffer.js';

const buildAttestation = function ({
id = databaseBuffer.getNextId(),
createdAt = new Date(),
templateName = '6eme-pdf',
key = ATTESTATIONS.SIXTH_GRADE,
} = {}) {
const values = {
id,
createdAt,
templateName,
key,
};

return databaseBuffer.pushInsertable({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const TABLE_NAME = 'attestations';
const COLUMN_NAME = 'key';

const up = async function (knex) {
await knex.schema.table(TABLE_NAME, function (table) {
table.string(COLUMN_NAME).notNullable().unique();
});
};

const down = async function (knex) {
await knex.schema.table(TABLE_NAME, function (table) {
table.dropColumn(COLUMN_NAME);
});
};

export { down, up };
4 changes: 3 additions & 1 deletion api/db/seeds/data/team-prescription/build-quests.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ATTESTATIONS } from '../../../../src/profile/domain/constants.js';
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';
Expand All @@ -8,7 +9,8 @@ async function createAttestationQuest(databasebuilder) {
const successfulUsers = await retrieveSuccessfulUsers(databasebuilder, campaigns);

const { id: rewardId } = await databasebuilder.factory.buildAttestation({
templateName: '6eme-pdf',
templateName: 'sixth-grade-attestation-template',
key: ATTESTATIONS.SIXTH_GRADE,
});

const questEligibilityRequirements = [
Expand Down
19 changes: 19 additions & 0 deletions api/src/profile/application/api/attestations-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as path from 'node:path';
import * as url from 'node:url';

import { usecases } from '../../domain/usecases/index.js';
import * as pdfWithFormSerializer from '../../infrastructure/serializers/pdf/pdf-with-form-serializer.js';

const __dirname = url.fileURLToPath(new URL('.', import.meta.url));

export const generateAttestations = async function ({
attestationKey,
userIds,
dependencies = { pdfWithFormSerializer },
}) {
const { templateName, data } = await usecases.getAttestationDataForUsers({ attestationKey, userIds });

const templatePath = path.join(__dirname, `../../infrastructure/serializers/pdf/templates/${templateName}.pdf`);

return dependencies.pdfWithFormSerializer.generate(templatePath, data);
};
3 changes: 3 additions & 0 deletions api/src/profile/domain/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const ATTESTATIONS = {
SIXTH_GRADE: 'SIXTH_GRADE',
};
3 changes: 2 additions & 1 deletion api/src/profile/domain/models/ProfileReward.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
export class ProfileReward {
constructor({ id, rewardId, rewardType } = {}) {
constructor({ id, rewardId, rewardType, createdAt } = {}) {
this.id = id;
this.rewardId = rewardId;
this.rewardType = rewardType;
this.createdAt = createdAt;
}
}
27 changes: 27 additions & 0 deletions api/src/profile/domain/models/User.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import capitalize from 'lodash/capitalize.js';

export class User {
id;
#firstName;
#lastName;

constructor({ id, firstName, lastName }) {
this.id = id;
this.#firstName = firstName;
this.#lastName = lastName;
}

get fullName() {
return capitalize(this.#firstName) + ' ' + this.#lastName.toUpperCase();
}

toForm(createdAt) {
const map = new Map();

map.set('fullName', this.fullName);
map.set('filename', this.fullName + Date.now());
map.set('date', createdAt);

return map;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export async function getAttestationDataForUsers({ attestationKey, userIds, userRepository, profileRewardRepository }) {
const users = await userRepository.getByIds({ userIds });
const profileRewards = await profileRewardRepository.getByAttestationKeyAndUserIds({ attestationKey, userIds });

return profileRewards.map(({ userId, createdAt }) => {
const user = users.find((user) => user.id === userId);
return user.toForm(createdAt);
});
}
2 changes: 2 additions & 0 deletions api/src/profile/domain/usecases/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { fileURLToPath } from 'node:url';

import * as knowledgeElementRepository from '../../../../lib/infrastructure/repositories/knowledge-element-repository.js';
import * as competenceEvaluationRepository from '../../../evaluation/infrastructure/repositories/competence-evaluation-repository.js';
import { repositories } from '../../../profile/infrastructure/repositories/index.js';
import * as profileRewardRepository from '../../../profile/infrastructure/repositories/profile-reward-repository.js';
import * as areaRepository from '../../../shared/infrastructure/repositories/area-repository.js';
import * as competenceRepository from '../../../shared/infrastructure/repositories/competence-repository.js';
Expand All @@ -21,6 +22,7 @@ const dependencies = {
competenceEvaluationRepository,
knowledgeElementRepository,
profileRewardRepository,
userRepository: repositories.userRepository,
};

const usecases = injectDependencies(usecasesWithoutInjectedDependencies, dependencies);
Expand Down
15 changes: 15 additions & 0 deletions api/src/profile/infrastructure/repositories/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as usersApi from '../../../../src/identity-access-management/application/api/users-api.js';
import { injectDependencies } from '../../../../src/shared/infrastructure/utils/dependency-injection.js';
import * as userRepository from './user-repository.js';

const repositoriesWithoutInjectedDependencies = {
userRepository,
};

const dependencies = {
usersApi,
};

const repositories = injectDependencies(repositoriesWithoutInjectedDependencies, dependencies);

export { repositories };
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import { REWARD_TYPES } from '../../../quest/domain/constants.js';
import { DomainTransaction } from '../../../shared/domain/DomainTransaction.js';
import { ProfileReward } from '../../domain/models/ProfileReward.js';

const ATTESTATIONS_TABLE_NAME = 'attestations';

/**
* @param {number} userId
* @param {number} rewardId
* @param {('ATTESTATION')} rewardType
* @returns {Promise<*>}
* @param {Object} args
* @param {number} args.userId
* @param {number} args.rewardId
* @param {('ATTESTATION')} args.rewardType
* @returns {Promise<void>}
*/
export const save = async ({ userId, rewardId, rewardType = REWARD_TYPES.ATTESTATION }) => {
const knexConnection = await DomainTransaction.getConnection();
Expand All @@ -19,15 +22,32 @@ export const save = async ({ userId, rewardId, rewardType = REWARD_TYPES.ATTESTA
};

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

/**
* @param {Object} args
* @param {string} args.attestationKey
* @param {Array<number>} args.userIds
* @returns {Promise<Array<ProfileReward>>}
*/
export const getByAttestationKeyAndUserIds = async ({ attestationKey, userIds }) => {
const knexConnection = await DomainTransaction.getConnection();
const profileRewards = await knexConnection(PROFILE_REWARDS_TABLE_NAME)
.select(PROFILE_REWARDS_TABLE_NAME + '.*')
.join(ATTESTATIONS_TABLE_NAME, ATTESTATIONS_TABLE_NAME + '.id', PROFILE_REWARDS_TABLE_NAME + '.rewardId')
.whereIn('userId', userIds)
.where(ATTESTATIONS_TABLE_NAME + '.key', attestationKey);
return profileRewards.map(toDomain);
};

const toDomain = (profileReward) => {
return new ProfileReward(profileReward);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { User } from '../../domain/models/User.js';

export async function getByIds({ userIds, usersApi }) {
const userDTOs = await usersApi.getUserDetailsByUserIds({ userIds });

return userDTOs.map((userDTO) => new User(userDTO));
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { User } from '../../../../../src/profile/domain/models/User.js';
import { usecases } from '../../../../../src/profile/domain/usecases/index.js';
import { databaseBuilder, expect, sinon } from '../../../../test-helper.js';

describe('Profile | Integration | Domain | get-attestation-data-for-users', function () {
let clock;
const now = new Date('2022-12-25');

beforeEach(function () {
clock = sinon.useFakeTimers({
now,
toFake: ['Date'],
});
});

afterEach(async function () {
clock.restore();
});

describe('#getAttestationDataForUsers', function () {
it('should return profile rewards', async function () {
const attestation = databaseBuilder.factory.buildAttestation();
const firstUser = new User(databaseBuilder.factory.buildUser());
const secondUser = new User(databaseBuilder.factory.buildUser());
const firstCreatedAt = databaseBuilder.factory.buildProfileReward({
rewardId: attestation.id,
userId: firstUser.id,
}).createdAt;
const secondCreatedAt = databaseBuilder.factory.buildProfileReward({
rewardId: attestation.id,
userId: secondUser.id,
}).createdAt;

await databaseBuilder.commit();

const results = await usecases.getAttestationDataForUsers({
attestationKey: attestation.key,
userIds: [firstUser.id, secondUser.id],
});

expect(results).to.deep.equal([firstUser.toForm(firstCreatedAt), secondUser.toForm(secondCreatedAt)]);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { PROFILE_REWARDS_TABLE_NAME } from '../../../../../db/migrations/20240820101213_add-profile-rewards-table.js';
import { ATTESTATIONS } from '../../../../../src/profile/domain/constants.js';
import { ProfileReward } from '../../../../../src/profile/domain/models/ProfileReward.js';
import { getByUserId, save } from '../../../../../src/profile/infrastructure/repositories/profile-reward-repository.js';
import {
getByAttestationKeyAndUserIds,
getByUserId,
save,
} from '../../../../../src/profile/infrastructure/repositories/profile-reward-repository.js';
import { REWARD_TYPES } from '../../../../../src/quest/domain/constants.js';
import { databaseBuilder, expect, knex } from '../../../../test-helper.js';

Expand Down Expand Up @@ -40,8 +45,13 @@ describe('Profile | Integration | Repository | profile-reward', function () {
eligibilityRequirements: {},
successRequirements: {},
});
const otherAttestation = databaseBuilder.factory.buildAttestation({
templateName: 'otherTemplateName',
key: 'otherKey',
});
const { rewardId: secondRewardId } = databaseBuilder.factory.buildQuest({
rewardType: REWARD_TYPES.ATTESTATION,
rewardId: otherAttestation.id,
eligibilityRequirements: {},
successRequirements: {},
});
Expand Down Expand Up @@ -83,4 +93,90 @@ describe('Profile | Integration | Repository | profile-reward', function () {
expect(result).to.be.empty;
});
});

describe('#getByAttestationKeyAndUserIds', function () {
it('should return an empty array if there are no attestations for these users', async function () {
// given
const attestation = databaseBuilder.factory.buildAttestation();
const user = databaseBuilder.factory.buildUser();
await databaseBuilder.commit();

// when
const result = await getByAttestationKeyAndUserIds({ attestationKey: attestation.key, userIds: [user.id] });

// then
expect(result.length).to.equal(0);
});

it('should return all attestations for users', async function () {
// given
const attestation = databaseBuilder.factory.buildAttestation();
const firstUser = databaseBuilder.factory.buildUser();
const secondUser = databaseBuilder.factory.buildUser();
const expectedProfileRewards = [];
expectedProfileRewards.push(
new ProfileReward(
databaseBuilder.factory.buildProfileReward({ rewardId: attestation.id, userId: firstUser.id }),
),
);
expectedProfileRewards.push(
new ProfileReward(
databaseBuilder.factory.buildProfileReward({ rewardId: attestation.id, userId: secondUser.id }),
),
);
await databaseBuilder.commit();

// when
const result = await getByAttestationKeyAndUserIds({
attestationKey: attestation.key,
userIds: [firstUser.id, secondUser.id],
});

// then
expect(result).to.be.deep.equal(expectedProfileRewards);
expect(result[0]).to.be.an.instanceof(ProfileReward);
expect(result[1]).to.be.an.instanceof(ProfileReward);
});

it('should not return attestations of other users', async function () {
// given
const attestation = databaseBuilder.factory.buildAttestation();
const firstUser = databaseBuilder.factory.buildUser();
const secondUser = databaseBuilder.factory.buildUser();
const expectedFirstUserProfileReward = [];
expectedFirstUserProfileReward.push(
new ProfileReward(
databaseBuilder.factory.buildProfileReward({ rewardId: attestation.id, userId: firstUser.id }),
),
);
databaseBuilder.factory.buildProfileReward({ rewardId: attestation.id, userId: secondUser.id });
await databaseBuilder.commit();

// when
const result = await getByAttestationKeyAndUserIds({
attestationKey: attestation.key,
userIds: [firstUser.id],
});

// then
expect(result).to.be.deep.equal(expectedFirstUserProfileReward);
});

it('should not return other attestations', async function () {
// given
const attestation = databaseBuilder.factory.buildAttestation({ key: ATTESTATIONS.SIXTH_GRADE });
const firstUser = databaseBuilder.factory.buildUser();
databaseBuilder.factory.buildProfileReward({ rewardId: attestation.id, userId: firstUser.id });
await databaseBuilder.commit();

// when
const result = await getByAttestationKeyAndUserIds({
attestationKey: 'SOME_KEY',
userIds: [firstUser.id],
});

// then
expect(result.length).to.equal(0);
});
});
});
Loading

0 comments on commit 4ce3ee0

Please sign in to comment.