diff --git a/api/db/migrations/20241126150201_update-constraint-national-student-id-organization-id-deleted-at.js b/api/db/migrations/20241126150201_update-constraint-national-student-id-organization-id-deleted-at.js new file mode 100644 index 00000000000..ad2109248f2 --- /dev/null +++ b/api/db/migrations/20241126150201_update-constraint-national-student-id-organization-id-deleted-at.js @@ -0,0 +1,32 @@ +const TABLE_NAME = 'organization-learners'; +const NEW_CONSTRAINT_NAME = 'one_active_sco_organization_learner'; +const DELETEDAT_COLUMN = 'deletedAt'; +const NATIONAL_STUDENT_ID_COLUMN = 'nationalStudentId'; +const ORGANIZATIONID_COLUMN = 'organizationId'; + +const up = async function (knex) { + await knex.schema.table(TABLE_NAME, (table) => { + table.dropUnique(['organizationId', 'nationalStudentId']); + }); + + return knex.raw( + `CREATE UNIQUE INDEX :name: ON :table: (:nationalStudentId:, :organizationId: ) WHERE :deletedAt: IS NULL;`, + { + name: NEW_CONSTRAINT_NAME, + table: TABLE_NAME, + nationalStudentId: NATIONAL_STUDENT_ID_COLUMN, + organizationId: ORGANIZATIONID_COLUMN, + deletedAt: DELETEDAT_COLUMN, + }, + ); +}; + +const down = async function (knex) { + await knex.raw(`DROP INDEX :name:;`, { name: NEW_CONSTRAINT_NAME }); + + return knex.schema.table(TABLE_NAME, (table) => { + table.unique(['organizationId', 'nationalStudentId']); + }); +}; + +export { down, up }; diff --git a/api/src/prescription/learner-management/infrastructure/repositories/organization-learner-repository.js b/api/src/prescription/learner-management/infrastructure/repositories/organization-learner-repository.js index 42c63a26384..54dbdc45083 100644 --- a/api/src/prescription/learner-management/infrastructure/repositories/organization-learner-repository.js +++ b/api/src/prescription/learner-management/infrastructure/repositories/organization-learner-repository.js @@ -97,7 +97,7 @@ const addOrUpdateOrganizationOfOrganizationLearners = async function (organizati await knexConn('organization-learners') .insert(organizationLearnersToSave) - .onConflict(['organizationId', 'nationalStudentId']) + .onConflict(knexConn.raw('("organizationId","nationalStudentId") where "deletedAt" is NULL')) .merge(); } catch (err) { throw new OrganizationLearnersCouldNotBeSavedError(); diff --git a/api/tests/prescription/learner-management/integration/infrastructure/repositories/organization-learner-repository_test.js b/api/tests/prescription/learner-management/integration/infrastructure/repositories/organization-learner-repository_test.js index c8456b3ed0d..6ff0d1f0ff1 100644 --- a/api/tests/prescription/learner-management/integration/infrastructure/repositories/organization-learner-repository_test.js +++ b/api/tests/prescription/learner-management/integration/infrastructure/repositories/organization-learner-repository_test.js @@ -580,6 +580,56 @@ describe('Integration | Repository | Organization Learner Management | Organizat }); }); + context('when there are deleted organizationLearners with same nationalStudentId', function () { + let organizationLearners; + let organizationId; + let firstOrganizationLearner; + + beforeEach(async function () { + organizationId = databaseBuilder.factory.buildOrganization().id; + + firstOrganizationLearner = new OrganizationLearner({ + lastName: 'Pipeau', + preferredLastName: 'Toto', + firstName: 'Corinne', + middleName: 'Dorothée', + thirdName: 'Driss', + sex: 'F', + birthdate: '2000-01-01', + birthCity: 'Perpi', + birthCityCode: '123456', + birthProvinceCode: '66', + birthCountryCode: '100', + MEFCode: 'MEF123456', + status: 'ST', + nationalStudentId: '1234', + division: '4B', + userId: null, + isDisabled: false, + organizationId, + }); + + databaseBuilder.factory.buildOrganizationLearner({ ...firstOrganizationLearner, deletedAt: new Date() }); + + await databaseBuilder.commit(); + + organizationLearners = [firstOrganizationLearner]; + }); + + it('should create all organizationLearners', async function () { + // when + await DomainTransaction.execute((domainTransaction) => { + return addOrUpdateOrganizationOfOrganizationLearners(organizationLearners, organizationId, domainTransaction); + }); + + // then + const actualOrganizationLearners = await organizationLearnerRepository.findByOrganizationId({ + organizationId, + }); + expect(actualOrganizationLearners).to.have.lengthOf(1); + }); + }); + context('when an organizationLearner is saved with a userId already present in organization', function () { it('should save the organization learner with userId as null', async function () { const { id: organizationId } = databaseBuilder.factory.buildOrganization();