diff --git a/api/db/database-builder/factory/build-legal-document-version.js b/api/db/database-builder/factory/build-legal-document-version.js new file mode 100644 index 00000000000..df7241d4a06 --- /dev/null +++ b/api/db/database-builder/factory/build-legal-document-version.js @@ -0,0 +1,15 @@ +import { databaseBuffer } from '../database-buffer.js'; + +const buildLegalDocumentVersion = function ({ + id = databaseBuffer.getNextId(), + type, + service, + versionAt = new Date(), +} = {}) { + return databaseBuffer.pushInsertable({ + tableName: 'legal-document-versions', + values: { id, type, service, versionAt }, + }); +}; + +export { buildLegalDocumentVersion }; diff --git a/api/src/legal-documents/application/api/legal-documents-api.js b/api/src/legal-documents/application/api/legal-documents-api.js new file mode 100644 index 00000000000..7c0829cbaf9 --- /dev/null +++ b/api/src/legal-documents/application/api/legal-documents-api.js @@ -0,0 +1,16 @@ +import { usecases } from '../../domain/usecases/index.js'; + +/** + * Accept legal document by user id. + * + * @param{string} params.service + * @param{string} params.type + * @param{string} params.userId + * + * @returns {Promise} + */ +const acceptLegalDocumentByUserId = async ({ type, service, userId }) => { + return usecases.acceptLegalDocumentByUserId({ type, service, userId }); +}; + +export { acceptLegalDocumentByUserId }; diff --git a/api/src/legal-documents/domain/models/LegalDocument.js b/api/src/legal-documents/domain/models/LegalDocument.js new file mode 100644 index 00000000000..85840b7e2e8 --- /dev/null +++ b/api/src/legal-documents/domain/models/LegalDocument.js @@ -0,0 +1,18 @@ +export class LegalDocument { + static TYPES = { + TOS: 'TOS', + }; + + static SERVICES = { + PIX_APP: 'pix-app', + PIX_ORGA: 'pix-orga', + PIX_CERTIF: 'pix-certif', + }; + + constructor({ id, type, service, versionAt }) { + this.id = id; + this.type = type; + this.service = service; + this.versionAt = versionAt; + } +} diff --git a/api/src/legal-documents/domain/usecases/accept-legal-document-by-user-id.usecase.js b/api/src/legal-documents/domain/usecases/accept-legal-document-by-user-id.usecase.js new file mode 100644 index 00000000000..b1006b213a6 --- /dev/null +++ b/api/src/legal-documents/domain/usecases/accept-legal-document-by-user-id.usecase.js @@ -0,0 +1,29 @@ +import { LegalDocument } from '../models/LegalDocument.js'; + +const { TOS } = LegalDocument.TYPES; +const { PIX_ORGA } = LegalDocument.SERVICES; + +const acceptLegalDocumentByUserId = async ({ + type, + service, + userId, + userRepository, + legalDocumentRepository, + userAcceptanceRepository, + logger, +}) => { + // legacy document acceptance + if (type === TOS && service === PIX_ORGA) { + await userRepository.setPixOrgaCguByUserId(userId); + } + + // new document acceptance + const document = await legalDocumentRepository.getLastVersionByTypeAndService({ type, service }); + if (!document) { + logger.warn(`No legal document found for type: ${type} and service: ${service}`); + } else { + await userAcceptanceRepository.create({ userId, legalDocumentVersionId: document.id }); + } +}; + +export { acceptLegalDocumentByUserId }; diff --git a/api/src/legal-documents/domain/usecases/index.js b/api/src/legal-documents/domain/usecases/index.js new file mode 100644 index 00000000000..57d2d409449 --- /dev/null +++ b/api/src/legal-documents/domain/usecases/index.js @@ -0,0 +1,28 @@ +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { config } from '../../../shared/config.js'; +import { injectDependencies } from '../../../shared/infrastructure/utils/dependency-injection.js'; +import { importNamedExportsFromDirectory } from '../../../shared/infrastructure/utils/import-named-exports-from-directory.js'; +import { logger } from '../../../shared/infrastructure/utils/logger.js'; +import * as legalDocumentRepository from '../../infrastructure/repositories/legal-document.repository.js'; +import * as userRepository from '../../infrastructure/repositories/user.repository.js'; +import * as userAcceptanceRepository from '../../infrastructure/repositories/user-acceptance.repository.js'; + +const path = dirname(fileURLToPath(import.meta.url)); + +const repositories = { + legalDocumentRepository, + userAcceptanceRepository, + userRepository, +}; + +const dependencies = Object.assign({ config, logger }, repositories); + +const usecasesWithoutInjectedDependencies = { + ...(await importNamedExportsFromDirectory({ path: join(path, './'), ignoredFileNames: ['index.js'] })), +}; + +const usecases = injectDependencies(usecasesWithoutInjectedDependencies, dependencies); + +export { usecases }; diff --git a/api/src/legal-documents/infrastructure/repositories/legal-document.repository.js b/api/src/legal-documents/infrastructure/repositories/legal-document.repository.js new file mode 100644 index 00000000000..09d49f877a7 --- /dev/null +++ b/api/src/legal-documents/infrastructure/repositories/legal-document.repository.js @@ -0,0 +1,18 @@ +import { DomainTransaction } from '../../../shared/domain/DomainTransaction.js'; +import { LegalDocument } from '../../domain/models/LegalDocument.js'; + +const getLastVersionByTypeAndService = async ({ type, service }) => { + const knexConnection = DomainTransaction.getConnection(); + const document = await knexConnection('legal-document-versions') + .where({ type, service }) + .orderBy('versionAt', 'desc') + .first(); + + if (!document) { + return null; + } + + return new LegalDocument(document); +}; + +export { getLastVersionByTypeAndService }; diff --git a/api/src/legal-documents/infrastructure/repositories/user-acceptance.repository.js b/api/src/legal-documents/infrastructure/repositories/user-acceptance.repository.js new file mode 100644 index 00000000000..4f08ef8bf11 --- /dev/null +++ b/api/src/legal-documents/infrastructure/repositories/user-acceptance.repository.js @@ -0,0 +1,8 @@ +import { DomainTransaction } from '../../../shared/domain/DomainTransaction.js'; + +const create = async ({ userId, legalDocumentVersionId }) => { + const knexConnection = DomainTransaction.getConnection(); + await knexConnection('legal-document-version-user-acceptances').insert({ userId, legalDocumentVersionId }); +}; + +export { create }; diff --git a/api/src/legal-documents/infrastructure/repositories/user.repository.js b/api/src/legal-documents/infrastructure/repositories/user.repository.js new file mode 100644 index 00000000000..8ab95230790 --- /dev/null +++ b/api/src/legal-documents/infrastructure/repositories/user.repository.js @@ -0,0 +1,11 @@ +import { DomainTransaction } from '../../../shared/domain/DomainTransaction.js'; + +const setPixOrgaCguByUserId = async (userId) => { + const knexConnection = DomainTransaction.getConnection(); + await knexConnection('users').where('id', userId).update({ + pixOrgaTermsOfServiceAccepted: true, + lastPixOrgaTermsOfServiceValidatedAt: new Date(), + }); +}; + +export { setPixOrgaCguByUserId }; diff --git a/api/tests/legal-documents/integration/application/api/legal-documents-api.test.js b/api/tests/legal-documents/integration/application/api/legal-documents-api.test.js new file mode 100644 index 00000000000..1cf50f0b6ff --- /dev/null +++ b/api/tests/legal-documents/integration/application/api/legal-documents-api.test.js @@ -0,0 +1,43 @@ +import * as legalDocumentsApi from '../../../../../src/legal-documents/application/api/legal-documents-api.js'; +import { LegalDocument } from '../../../../../src/legal-documents/domain/models/LegalDocument.js'; +import { databaseBuilder, expect, knex } from '../../../../test-helper.js'; + +const { TOS } = LegalDocument.TYPES; +const { PIX_ORGA } = LegalDocument.SERVICES; + +describe('Integration | Privacy | Application | Api | legal documents', function () { + describe('#acceptLegalDocumentByUserId', function () { + it('accepts the latest legal document version by user id ', async function () { + // given + const userId = databaseBuilder.factory.buildUser().id; + databaseBuilder.factory.buildLegalDocumentVersion({ + type: TOS, + service: PIX_ORGA, + versionAt: new Date('2021-01-01'), + }); + + const latestDocument = databaseBuilder.factory.buildLegalDocumentVersion({ + type: TOS, + service: PIX_ORGA, + versionAt: new Date(), + }); + + await databaseBuilder.commit(); + + const formerAcceptances = await knex('legal-document-version-user-acceptances').where({ userId }); + + expect(formerAcceptances.length).to.equal(0); + + // when + await legalDocumentsApi.acceptLegalDocumentByUserId({ userId, type: TOS, service: PIX_ORGA }); + + // then + const userAcceptance = await knex('legal-document-version-user-acceptances') + .where({ userId }) + .where('legalDocumentVersionId', latestDocument.id) + .first(); + + expect(userAcceptance).to.exist; + }); + }); +}); diff --git a/api/tests/legal-documents/integration/domain/usecases/accept-legal-document-by-user-id.usecase.test.js b/api/tests/legal-documents/integration/domain/usecases/accept-legal-document-by-user-id.usecase.test.js new file mode 100644 index 00000000000..c9722dea088 --- /dev/null +++ b/api/tests/legal-documents/integration/domain/usecases/accept-legal-document-by-user-id.usecase.test.js @@ -0,0 +1,57 @@ +import { LegalDocument } from '../../../../../src/legal-documents/domain/models/LegalDocument.js'; +import { usecases } from '../../../../../src/legal-documents/domain/usecases/index.js'; +import { databaseBuilder, expect, knex, sinon } from '../../../../test-helper.js'; + +const { TOS } = LegalDocument.TYPES; +const { PIX_ORGA } = LegalDocument.SERVICES; + +describe('Integration | Legal documents | Domain | Use case | accept-legal-document-by-user-id', function () { + it('accepts a legal document for a user', async function () { + // given + const user = databaseBuilder.factory.buildUser(); + const document = databaseBuilder.factory.buildLegalDocumentVersion({ type: TOS, service: PIX_ORGA }); + await databaseBuilder.commit(); + + // when + await usecases.acceptLegalDocumentByUserId({ userId: user.id, type: TOS, service: PIX_ORGA }); + + // then + const userAcceptance = await knex('legal-document-version-user-acceptances') + .where('userId', user.id) + .where('legalDocumentVersionId', document.id) + .first(); + expect(userAcceptance).to.exist; + }); + + context('when the legal document is the Terms of Service for Pix Orga', function () { + it('accepts the Pix Orga CGUs in the legacy and legal document model', async function () { + // given + const user = databaseBuilder.factory.buildUser({ pixOrgaTermsOfServiceAccepted: false }); + databaseBuilder.factory.buildLegalDocumentVersion({ type: TOS, service: PIX_ORGA }); + + await databaseBuilder.commit(); + + // when + await usecases.acceptLegalDocumentByUserId({ userId: user.id, type: TOS, service: PIX_ORGA }); + + // then + const updatedUser = await knex('users').where('id', user.id).first(); + expect(updatedUser.pixOrgaTermsOfServiceAccepted).to.equal(true); + }); + + it('logs an error, when no legal document is found', async function () { + // given + const user = databaseBuilder.factory.buildUser({ pixOrgaTermsOfServiceAccepted: false }); + const loggerStub = { warn: sinon.stub() }; + await databaseBuilder.commit(); + + // when + await usecases.acceptLegalDocumentByUserId({ userId: user.id, type: TOS, service: PIX_ORGA, logger: loggerStub }); + + // then + expect(loggerStub.warn).to.have.been.calledWith( + `No legal document found for type: ${TOS} and service: ${PIX_ORGA}`, + ); + }); + }); +}); diff --git a/api/tests/legal-documents/integration/infrastructure/repositories/legal-document.repository.test.js b/api/tests/legal-documents/integration/infrastructure/repositories/legal-document.repository.test.js new file mode 100644 index 00000000000..808959a78d7 --- /dev/null +++ b/api/tests/legal-documents/integration/infrastructure/repositories/legal-document.repository.test.js @@ -0,0 +1,50 @@ +import { LegalDocument } from '../../../../../src/legal-documents/domain/models/LegalDocument.js'; +import * as legalDocumentRepository from '../../../../../src/legal-documents/infrastructure/repositories/legal-document.repository.js'; +import { databaseBuilder, domainBuilder, expect } from '../../../../test-helper.js'; + +const { TOS } = LegalDocument.TYPES; +const { PIX_ORGA, PIX_APP } = LegalDocument.SERVICES; + +describe('Integration | Legal document | Infrastructure | Repository | legal-documents', function () { + describe('#getLastVersionByTypeAndService', function () { + it('returns the last legal document version by type and service', async function () { + // given + const type = TOS; + const service = PIX_ORGA; + databaseBuilder.factory.buildLegalDocumentVersion({ + type, + service, + versionAt: new Date('2020-12-01'), + }); + const expectedDocument = databaseBuilder.factory.buildLegalDocumentVersion({ + type, + service, + versionAt: new Date('2024-12-01'), + }); + + databaseBuilder.factory.buildLegalDocumentVersion({ + type, + service: PIX_APP, + versionAt: new Date('2024-12-01'), + }); + await databaseBuilder.commit(); + + // when + const lastDocument = await legalDocumentRepository.getLastVersionByTypeAndService({ type, service }); + + // then + expect(lastDocument).to.deepEqualInstance(domainBuilder.buildLegalDocument(expectedDocument)); + }); + + it('returns null when no document found', async function () { + // when + const lastDocument = await legalDocumentRepository.getLastVersionByTypeAndService({ + type: 'toto', + service: 'tutu', + }); + + // then + expect(lastDocument).to.be.null; + }); + }); +}); diff --git a/api/tests/legal-documents/integration/infrastructure/repositories/user-acceptance.repository.test.js b/api/tests/legal-documents/integration/infrastructure/repositories/user-acceptance.repository.test.js new file mode 100644 index 00000000000..9b9a80ce920 --- /dev/null +++ b/api/tests/legal-documents/integration/infrastructure/repositories/user-acceptance.repository.test.js @@ -0,0 +1,27 @@ +import { LegalDocument } from '../../../../../src/legal-documents/domain/models/LegalDocument.js'; +import * as userAcceptanceRepository from '../../../../../src/legal-documents/infrastructure/repositories/user-acceptance.repository.js'; +import { databaseBuilder, expect, knex } from '../../../../test-helper.js'; + +const { TOS } = LegalDocument.TYPES; +const { PIX_ORGA } = LegalDocument.SERVICES; + +describe('Integration | Legal document | Infrastructure | Repository | user-acceptance', function () { + describe('#create', function () { + it('creates an acceptance record for a user and legal document', async function () { + // given + const user = databaseBuilder.factory.buildUser(); + const document = databaseBuilder.factory.buildLegalDocumentVersion({ type: TOS, service: PIX_ORGA }); + await databaseBuilder.commit(); + + // when + await userAcceptanceRepository.create({ userId: user.id, legalDocumentVersionId: document.id }); + + // then + const userAcceptance = await knex('legal-document-version-user-acceptances') + .where('userId', user.id) + .where('legalDocumentVersionId', document.id) + .first(); + expect(userAcceptance.acceptedAt).to.be.a('date'); + }); + }); +}); diff --git a/api/tests/legal-documents/integration/infrastructure/repositories/user.repository.test.js b/api/tests/legal-documents/integration/infrastructure/repositories/user.repository.test.js new file mode 100644 index 00000000000..17a3df7961f --- /dev/null +++ b/api/tests/legal-documents/integration/infrastructure/repositories/user.repository.test.js @@ -0,0 +1,23 @@ +import * as userRepository from '../../../../../src/legal-documents/infrastructure/repositories/user.repository.js'; +import { databaseBuilder, expect, knex } from '../../../../test-helper.js'; + +describe('Integration | Legal document | Infrastructure | Repository | user', function () { + describe('#setPixOrgaCguByUserId', function () { + it('sets the Pix Orga CGU for a user id', async function () { + // given + const user = databaseBuilder.factory.buildUser({ + pixOrgaTermsOfServiceAccepted: false, + lastPixOrgaTermsOfServiceValidatedAt: null, + }); + await databaseBuilder.commit(); + + // when + await userRepository.setPixOrgaCguByUserId(user.id); + + // then + const updatedUser = await knex('users').where('id', user.id).first(); + expect(updatedUser.pixOrgaTermsOfServiceAccepted).to.equal(true); + expect(updatedUser.lastPixOrgaTermsOfServiceValidatedAt).to.be.a('date'); + }); + }); +}); diff --git a/api/tests/tooling/domain-builder/factory/build-legal-document.js b/api/tests/tooling/domain-builder/factory/build-legal-document.js new file mode 100644 index 00000000000..e825ef9b853 --- /dev/null +++ b/api/tests/tooling/domain-builder/factory/build-legal-document.js @@ -0,0 +1,17 @@ +import { LegalDocument } from '../../../../src/legal-documents/domain/models/LegalDocument.js'; + +const buildLegalDocument = function ({ + id = 123, + type = LegalDocument.TYPES.TOS, + service = LegalDocument.SERVICES.PIX_APP, + versionAt = new Date(), +} = {}) { + return new LegalDocument({ + id, + type, + service, + versionAt, + }); +}; + +export { buildLegalDocument }; diff --git a/api/tests/tooling/domain-builder/factory/index.js b/api/tests/tooling/domain-builder/factory/index.js index a969c5c1113..5864481af39 100644 --- a/api/tests/tooling/domain-builder/factory/index.js +++ b/api/tests/tooling/domain-builder/factory/index.js @@ -101,6 +101,7 @@ import { buildJuryCertificationSummary } from './build-jury-certification-summar import { buildJurySession } from './build-jury-session.js'; import { buildKnowledgeElement } from './build-knowledge-element.js'; import { buildLearningContent } from './build-learning-content.js'; +import { buildLegalDocument } from './build-legal-document.js'; import { buildMembership } from './build-membership.js'; import { buildMission } from './build-mission.js'; import { buildOrganization } from './build-organization.js'; @@ -365,6 +366,7 @@ export { buildJurySession, buildKnowledgeElement, buildLearningContent, + buildLegalDocument, buildMembership, buildMission, buildOrganization,