diff --git a/back/src/adapters/primary/routers/convention/convention.e2e.test.ts b/back/src/adapters/primary/routers/convention/convention.e2e.test.ts index aed241f668..95e77c9f66 100644 --- a/back/src/adapters/primary/routers/convention/convention.e2e.test.ts +++ b/back/src/adapters/primary/routers/convention/convention.e2e.test.ts @@ -394,8 +394,8 @@ describe("convention e2e", () => { status: 404, body: { status: 404, - message: - "No convention found with id add5c20e-6dd2-45af-affe-927358005251", + message: errors.convention.notFound({ conventionId: unknownId }) + .message, }, }); }); diff --git a/back/src/domains/convention/use-cases/GetConvention.ts b/back/src/domains/convention/use-cases/GetConvention.ts index d7cc02df19..634a786735 100644 --- a/back/src/domains/convention/use-cases/GetConvention.ts +++ b/back/src/domains/convention/use-cases/GetConvention.ts @@ -1,12 +1,10 @@ import { AgencyWithUsersRights, - ConventionDomainPayload, - ConventionId, ConventionReadDto, ConventionRelatedJwtPayload, - ForbiddenError, - InclusionConnectJwtPayload, - NotFoundError, + EmailHash, + Role, + UserId, WithConventionId, errors, getIcUserRoleForAccessingConvention, @@ -33,59 +31,44 @@ export class GetConvention extends TransactionalUseCase< uow: UnitOfWork, authPayload?: ConventionRelatedJwtPayload, ): Promise { - if (!authPayload) { - throw new ForbiddenError("No auth payload provided"); - } + if (!authPayload) throw errors.user.noJwtProvided(); const convention = await uow.conventionQueries.getConventionById(conventionId); - if (!convention) - throw new NotFoundError(`No convention found with id ${conventionId}`); - - const isConventionDomainPayload = "emailHash" in authPayload; - const isInclusionConnectPayload = this.#isInclusionConnectPayload( - authPayload, - conventionId, - ); - - const agency = await uow.agencyRepository.getById(convention.agencyId); - if (!agency) { - throw new NotFoundError(`Agency ${convention.agencyId} not found`); - } + if (!convention) throw errors.convention.notFound({ conventionId }); - if (isConventionDomainPayload) { - return this.#onConventionDomainPayload({ - authPayload, - uow, - convention, - agency, - }); - } - - if (isInclusionConnectPayload) { - return this.#onInclusionConnectPayload({ - authPayload, - uow, - convention, - }); - } - - throw new ForbiddenError("Incorrect jwt"); + return "emailHash" in authPayload + ? this.#onConventionDomainPayload({ + emailHash: authPayload.emailHash, + role: authPayload.role, + uow, + convention, + }) + : this.#onInclusionConnectPayload({ + userId: authPayload.userId, + uow, + convention, + }); } async #onConventionDomainPayload({ - authPayload, + role, + emailHash, convention, - agency, uow, }: { - authPayload: ConventionDomainPayload; + role: Role; + emailHash: EmailHash; convention: ConventionReadDto; - agency: AgencyWithUsersRights; uow: UnitOfWork; }): Promise { + const agency = await uow.agencyRepository.getById(convention.agencyId); + if (!agency) + throw errors.agency.notFound({ agencyId: convention.agencyId }); + const isMatchingEmailHash = await this.#isEmailHashMatch({ - authPayload, + emailHash, + role, convention, agency, uow, @@ -100,70 +83,69 @@ export class GetConvention extends TransactionalUseCase< } async #onInclusionConnectPayload({ - authPayload, + userId, convention, uow, }: { - authPayload: InclusionConnectJwtPayload; + userId: UserId; convention: ConventionReadDto; uow: UnitOfWork; }): Promise { - const roles = getIcUserRoleForAccessingConvention( - convention, - await getIcUserByUserId(uow, authPayload.userId), - ); + const user = await getIcUserByUserId(uow, userId); - if (!roles.length) - throw new ForbiddenError( - `User with id '${authPayload.userId}' is not allowed to access convention with id '${convention.id}'`, - ); + const roles = getIcUserRoleForAccessingConvention(convention, user); + if (roles.length) return convention; - return convention; - } + const establishment = + await uow.establishmentAggregateRepository.getEstablishmentAggregateBySiret( + convention.siret, + ); - #isInclusionConnectPayload( - authPayload: ConventionRelatedJwtPayload, - conventionId: ConventionId, - ): authPayload is InclusionConnectJwtPayload { - if (!("role" in authPayload)) return true; - if (authPayload.role === "back-office") return false; - if (authPayload.applicationId === conventionId) return false; - throw new ForbiddenError( - `This token is not allowed to access convention with id ${conventionId}. Role was '${authPayload.role}'`, + const hasSomeEstablishmentRights = establishment?.userRights.some( + (userRight) => userRight.userId === user.id, ); + + if (hasSomeEstablishmentRights) return convention; + + throw errors.convention.forbiddenMissingRights({ + conventionId: convention.id, + userId: user.id, + }); } async #isEmailHashMatch({ - authPayload, + emailHash, + role, convention, agency, uow, }: { - authPayload: ConventionDomainPayload; + emailHash: EmailHash; + role: Role; convention: ConventionReadDto; agency: AgencyWithUsersRights; uow: UnitOfWork; }): Promise { - const isMatchingConventionEmails = await isHashMatchConventionEmails({ + const isEmailMatchingPeAdvisor = isHashMatchPeAdvisorEmail({ convention, + emailHash, + }); + if (isEmailMatchingPeAdvisor) return true; + + const isMatchingConventionEmails = await isHashMatchConventionEmails({ uow, + role, + emailHash, + convention, agency, - authPayload, }); - const isEmailMatchingIcUserEmails = - await isHashMatchNotNotifiedCounsellorOrValidator({ - authPayload, - agency, - uow, - }); - const isEmailMatchingPeAdvisor = isHashMatchPeAdvisorEmail({ - convention, - authPayload, + if (isMatchingConventionEmails) return true; + + return await isHashMatchNotNotifiedCounsellorOrValidator({ + uow, + emailHash, + agency, + role, }); - return ( - isMatchingConventionEmails || - isEmailMatchingIcUserEmails || - isEmailMatchingPeAdvisor - ); } } diff --git a/back/src/domains/convention/use-cases/GetConvention.unit.test.ts b/back/src/domains/convention/use-cases/GetConvention.unit.test.ts index e9ba8e1498..e45583501b 100644 --- a/back/src/domains/convention/use-cases/GetConvention.unit.test.ts +++ b/back/src/domains/convention/use-cases/GetConvention.unit.test.ts @@ -1,14 +1,11 @@ import { AgencyDtoBuilder, ConventionDtoBuilder, - ConventionJwtPayload, - ForbiddenError, - InclusionConnectDomainJwtPayload, InclusionConnectedUserBuilder, - NotFoundError, Role, User, errors, + establishmentsRoles, expectPromiseToFailWithError, expectToEqual, makeEmailHash, @@ -19,9 +16,12 @@ import { InMemoryUnitOfWork, createInMemoryUow, } from "../../core/unit-of-work/adapters/createInMemoryUow"; +import { UuidV4Generator } from "../../core/uuid-generator/adapters/UuidGeneratorImplementations"; +import { EstablishmentAggregateBuilder } from "../../establishment/helpers/EstablishmentBuilders"; import { GetConvention } from "./GetConvention"; describe("Get Convention", () => { + const uuidGenerator = new UuidV4Generator(); const counsellor = new InclusionConnectedUserBuilder() .withId("counsellor") .withEmail("counsellor@mail.fr") @@ -30,12 +30,72 @@ describe("Get Convention", () => { .withId("validator") .withEmail("validator@mail.fr") .build(); + const johnDoe: User = { + id: "johndoe", + email: "my-user@email.com", + firstName: "John", + lastName: "Doe", + externalId: "john-external-id", + createdAt: new Date().toISOString(), + }; + const establishmentRep: User = { + id: "estabrep", + email: "estabrep@mail.com", + firstName: "John", + lastName: "Doe", + externalId: "john-external-id", + createdAt: new Date().toISOString(), + }; + const tutor: User = { + id: "my-tutor-user-id", + email: "tutor@email.com", + firstName: "John", + lastName: "Doe", + externalId: "john-tutor-external-id", + createdAt: new Date().toISOString(), + }; + + const backofficeAdminUser = new InclusionConnectedUserBuilder() + .withId(uuidGenerator.new()) + .withIsAdmin(true) + .buildUser(); + const agency = new AgencyDtoBuilder().build(); - const convention = new ConventionDtoBuilder().withAgencyId(agency.id).build(); + const convention = new ConventionDtoBuilder() + .withId(uuidGenerator.new()) + .withAgencyId(agency.id) + .withEstablishmentRepresentative({ + email: "estab-rep@email.com", + firstName: "", + lastName: "", + phone: "", + role: "establishment-representative", + }) + .withBeneficiaryRepresentative({ + email: "benef-rep@email.com", + firstName: "", + lastName: "", + phone: "", + role: "beneficiary-representative", + }) + .withBeneficiaryCurrentEmployer({ + email: "benef-rep@email.com", + firstName: "", + lastName: "", + phone: "", + businessAddress: "", + businessName: "", + businessSiret: "", + job: "", + role: "beneficiary-current-employer", + }) + .withEstablishmentRepresentativeEmail(establishmentRep.email) + .build(); const conventionWithEstablishmentTutor = new ConventionDtoBuilder() + .withId(uuidGenerator.new()) .withAgencyId(agency.id) .withEstablishmentTutor({ - email: "establishment-tutor@mail.fr", + email: tutor.email, firstName: "John", lastName: "Doe", role: "establishment-tutor", @@ -43,12 +103,56 @@ describe("Get Convention", () => { job: "Job", }) .build(); + const establishmentWithSiret = new EstablishmentAggregateBuilder() + .withEstablishmentSiret(convention.siret) + .withUserRights([ + { + role: "establishment-admin", + job: "", + phone: "", + userId: tutor.id, + }, + ]) + .build(); + + const ftAdvisorEmail = "ft-advisor@mail.fr"; + const ftConnectedConvention = new ConventionDtoBuilder(convention) + .withId(uuidGenerator.new()) + .withFederatedIdentity({ + provider: "peConnect", + token: "some-id", + payload: { + advisor: { + email: ftAdvisorEmail, + firstName: "john", + lastName: "doe", + type: "PLACEMENT", + }, + }, + }) + .build(); + let getConvention: GetConvention; let uow: InMemoryUnitOfWork; beforeEach(() => { uow = createInMemoryUow(); getConvention = new GetConvention(new InMemoryUowPerformer(uow)); + + uow.conventionRepository.setConventions([ + convention, + conventionWithEstablishmentTutor, + ftConnectedConvention, + ]); + uow.agencyRepository.agencies = [toAgencyWithRights(agency)]; + uow.userRepository.users = [ + counsellor, + validator, + johnDoe, + establishmentRep, + tutor, + backofficeAdminUser, + ]; }); describe("Wrong paths", () => { @@ -56,44 +160,34 @@ describe("Get Convention", () => { it("When no auth payload provided", async () => { await expectPromiseToFailWithError( getConvention.execute({ conventionId: convention.id }), - new ForbiddenError("No auth payload provided"), + errors.user.noJwtProvided(), ); }); - it("When the user don't have correct role on inclusion connected users", async () => { - const user: User = { - id: "my-user-id", - email: "my-user@email.com", - firstName: "John", - lastName: "Doe", - externalId: "john-external-id", - createdAt: new Date().toISOString(), - }; - - uow.userRepository.users = [user]; + it("When the user don't have correct role on inclusion connected users neither has right on existing establishment with same siret in convention", async () => { + uow.establishmentAggregateRepository.establishmentAggregates = [ + establishmentWithSiret, + ]; uow.agencyRepository.agencies = [ toAgencyWithRights(agency, { - [user.id]: { isNotifiedByEmail: false, roles: ["to-review"] }, + [johnDoe.id]: { isNotifiedByEmail: false, roles: ["to-review"] }, }), ]; - uow.conventionRepository.setConventions([convention]); await expectPromiseToFailWithError( getConvention.execute( { conventionId: convention.id }, - { userId: "my-user-id" }, - ), - new ForbiddenError( - `User with id 'my-user-id' is not allowed to access convention with id '${convention.id}'`, + { userId: johnDoe.id }, ), + errors.convention.forbiddenMissingRights({ + conventionId: convention.id, + userId: johnDoe.id, + }), ); }); describe("with ConventionJwtPayload", () => { it("When convention id in jwt token does not match provided one", async () => { - uow.agencyRepository.agencies = [toAgencyWithRights(agency)]; - uow.conventionRepository.setConventions([convention]); - await expectPromiseToFailWithError( getConvention.execute( { conventionId: convention.id }, @@ -103,9 +197,9 @@ describe("Get Convention", () => { emailHash: "", }, ), - new ForbiddenError( - `This token is not allowed to access convention with id ${convention.id}. Role was 'establishment-representative'`, - ), + errors.convention.forbiddenMissingRights({ + conventionId: convention.id, + }), ); }); @@ -113,22 +207,36 @@ describe("Get Convention", () => { "validator", "beneficiary", "counsellor", + "validator", "establishment-representative", - ] as const)( - "When the user email for role %s is not used in the convention anymore", - async (role: Role) => { - uow.agencyRepository.agencies = [toAgencyWithRights(agency)]; - uow.conventionRepository.setConventions([convention]); - const payload: ConventionJwtPayload = { - role, - emailHash: "oldHash", - applicationId: convention.id, - iat: 1, - version: 1, - }; + "establishment-tutor", + "beneficiary-current-employer", + "beneficiary-representative", + ] satisfies Role[])( + "When there is not email hash match from '%role' emails in convention or in agency", + async (role) => { + uow.agencyRepository.agencies = [ + toAgencyWithRights(agency, { + [validator.id]: { + isNotifiedByEmail: false, + roles: ["validator"], + }, + [counsellor.id]: { + isNotifiedByEmail: false, + roles: ["counsellor"], + }, + }), + ]; await expectPromiseToFailWithError( - getConvention.execute({ conventionId: convention.id }, payload), + getConvention.execute( + { conventionId: convention.id }, + { + role, + emailHash: "thisHashDontMatch", + applicationId: convention.id, + }, + ), errors.convention.forbiddenMissingRights({ conventionId: convention.id, }), @@ -137,15 +245,6 @@ describe("Get Convention", () => { ); it("when the user has inclusion connect but not for the agency of this convention", async () => { - const user: User = { - id: "my-user-id", - email: "john@mail.com", - firstName: "John", - lastName: "Doe", - - externalId: "john-external-id", - createdAt: new Date().toISOString(), - }; const anotherAgency = new AgencyDtoBuilder(agency) .withId("another") .build(); @@ -153,22 +252,19 @@ describe("Get Convention", () => { uow.agencyRepository.agencies = [ toAgencyWithRights(agency), toAgencyWithRights(anotherAgency, { - [user.id]: { isNotifiedByEmail: false, roles: ["validator"] }, + [johnDoe.id]: { isNotifiedByEmail: false, roles: ["validator"] }, }), ]; - uow.conventionRepository.setConventions([convention]); - uow.userRepository.users = [user]; - - const payload: ConventionJwtPayload = { - role: "validator", - emailHash: makeEmailHash(user.email), - applicationId: convention.id, - iat: 1, - version: 1, - }; await expectPromiseToFailWithError( - getConvention.execute({ conventionId: convention.id }, payload), + getConvention.execute( + { conventionId: convention.id }, + { + role: "validator", + emailHash: makeEmailHash(johnDoe.email), + applicationId: convention.id, + }, + ), errors.convention.forbiddenMissingRights({ conventionId: convention.id, }), @@ -179,6 +275,8 @@ describe("Get Convention", () => { describe("Not found error", () => { it("When the Convention does not exist", async () => { + uow.conventionRepository.setConventions([]); + await expectPromiseToFailWithError( getConvention.execute( { conventionId: convention.id }, @@ -188,161 +286,157 @@ describe("Get Convention", () => { emailHash: "", }, ), - new NotFoundError(`No convention found with id ${convention.id}`), + errors.convention.notFound({ conventionId: convention.id }), ); }); - it("When if user is not on inclusion connected users", async () => { - uow.agencyRepository.agencies = [toAgencyWithRights(agency)]; - uow.conventionRepository.setConventions([convention]); - const userId = "my-user-id"; + it("When if user is missing", async () => { + uow.userRepository.users = []; await expectPromiseToFailWithError( - getConvention.execute({ conventionId: convention.id }, { userId }), - errors.user.notFound({ userId }), + getConvention.execute( + { conventionId: convention.id }, + { userId: johnDoe.id }, + ), + errors.user.notFound({ userId: johnDoe.id }), ); }); }); }); describe("Right paths", () => { - beforeEach(() => { - uow.conventionRepository.setConventions([convention]); - uow.agencyRepository.agencies = [toAgencyWithRights(agency)]; - }); - describe("Inclusion connected user", () => { it("that have agency rights", async () => { - const user: User = { - id: "my-user-id", - email: "my-user@email.com", - firstName: "John", - lastName: "Doe", - externalId: "john-external-id", - createdAt: new Date().toISOString(), - }; - - uow.userRepository.users = [user]; uow.agencyRepository.agencies = [ toAgencyWithRights(agency, { - [user.id]: { isNotifiedByEmail: false, roles: ["validator"] }, + [johnDoe.id]: { isNotifiedByEmail: false, roles: ["validator"] }, }), ]; - const fetchedConvention = await getConvention.execute( - { conventionId: convention.id }, + expectToEqual( + await getConvention.execute( + { conventionId: convention.id }, + { + userId: johnDoe.id, + }, + ), { - userId: user.id, + ...convention, + agencyName: agency.name, + agencyDepartment: agency.address.departmentCode, + agencyKind: agency.kind, + agencySiret: agency.agencySiret, + agencyCounsellorEmails: [], + agencyValidatorEmails: [johnDoe.email], }, ); - expectToEqual(fetchedConvention, { - ...convention, - agencyName: agency.name, - agencyDepartment: agency.address.departmentCode, - agencyKind: agency.kind, - agencySiret: agency.agencySiret, - agencyCounsellorEmails: [], - agencyValidatorEmails: [user.email], - }); }); - it("that establishment rep is also the inclusion connected user", async () => { - const user: User = { - id: "my-user-id", - email: convention.signatories.establishmentRepresentative.email, - firstName: "John", - lastName: "Doe", - externalId: "john-external-id", - createdAt: new Date().toISOString(), - }; - - uow.userRepository.users = [user]; - uow.agencyRepository.agencies = [toAgencyWithRights(agency)]; - - const jwtPayload: InclusionConnectDomainJwtPayload = { - userId: user.id, - }; - - const fetchedConvention = await getConvention.execute( - { conventionId: convention.id }, - jwtPayload, - ); + describe("establishment rights", () => { + it("that establishment rep email is also the inclusion connected user email", async () => { + expectToEqual( + await getConvention.execute( + { conventionId: convention.id }, + { + userId: establishmentRep.id, + }, + ), + { + ...convention, + agencyName: agency.name, + agencyDepartment: agency.address.departmentCode, + agencyKind: agency.kind, + agencySiret: agency.agencySiret, + agencyCounsellorEmails: agency.counsellorEmails, + agencyValidatorEmails: agency.validatorEmails, + }, + ); + }); - expectToEqual(fetchedConvention, { - ...convention, - agencyName: agency.name, - agencyDepartment: agency.address.departmentCode, - agencyKind: agency.kind, - agencySiret: agency.agencySiret, - agencyCounsellorEmails: agency.counsellorEmails, - agencyValidatorEmails: agency.validatorEmails, + it("that establishment tutor email is also the inclusion connected user email", async () => { + expectToEqual( + await getConvention.execute( + { conventionId: conventionWithEstablishmentTutor.id }, + { + userId: tutor.id, + }, + ), + { + ...conventionWithEstablishmentTutor, + agencyName: agency.name, + agencyDepartment: agency.address.departmentCode, + agencyKind: agency.kind, + agencySiret: agency.agencySiret, + agencyCounsellorEmails: agency.counsellorEmails, + agencyValidatorEmails: agency.validatorEmails, + }, + ); }); - }); - it("that establishment tutor is also the inclusion connected user", async () => { - uow.conventionRepository.setConventions([ - conventionWithEstablishmentTutor, - ]); - const user: User = { - id: "my-tutor-user-id", - email: conventionWithEstablishmentTutor.establishmentTutor.email, - firstName: "John", - lastName: "Doe", - externalId: "john-tutor-external-id", - createdAt: new Date().toISOString(), - }; - uow.userRepository.users = [user]; - uow.agencyRepository.agencies = [toAgencyWithRights(agency)]; - - const jwtPayload: InclusionConnectDomainJwtPayload = { - userId: user.id, - }; - - const fetchedConvention = await getConvention.execute( - { conventionId: conventionWithEstablishmentTutor.id }, - jwtPayload, + it.each(establishmentsRoles)( + "that the inclusion connected user is also %s of the existing establishment with same siret in convention", + async (role) => { + const establishmentWithRights = new EstablishmentAggregateBuilder( + establishmentWithSiret, + ) + .withUserRights([ + { + userId: johnDoe.id, + role, + job: "", + phone: "", + }, + ]) + .build(); + + uow.establishmentAggregateRepository.establishmentAggregates = [ + establishmentWithRights, + ]; + + expectToEqual( + await getConvention.execute( + { conventionId: convention.id }, + { + userId: johnDoe.id, + }, + ), + { + ...convention, + agencyName: agency.name, + agencyDepartment: agency.address.departmentCode, + agencyKind: agency.kind, + agencySiret: agency.agencySiret, + agencyCounsellorEmails: agency.counsellorEmails, + agencyValidatorEmails: agency.validatorEmails, + }, + ); + }, ); - - expectToEqual(fetchedConvention, { - ...conventionWithEstablishmentTutor, - agencyName: agency.name, - agencyDepartment: agency.address.departmentCode, - agencyKind: agency.kind, - agencySiret: agency.agencySiret, - agencyCounsellorEmails: agency.counsellorEmails, - agencyValidatorEmails: agency.validatorEmails, - }); }); it("the user is backofficeAdmin", async () => { - const backofficeAdminUser = new InclusionConnectedUserBuilder() - .withIsAdmin(true) - .buildUser(); - - uow.userRepository.users = [backofficeAdminUser]; - - const conventionResult = await getConvention.execute( - { conventionId: convention.id }, + expectToEqual( + await getConvention.execute( + { conventionId: convention.id }, + { + userId: backofficeAdminUser.id, + }, + ), { - userId: backofficeAdminUser.id, + ...convention, + agencyName: agency.name, + agencyDepartment: agency.address.departmentCode, + agencyKind: agency.kind, + agencySiret: agency.agencySiret, + agencyCounsellorEmails: agency.counsellorEmails, + agencyValidatorEmails: agency.validatorEmails, }, ); - - expectToEqual(conventionResult, { - ...convention, - agencyName: agency.name, - agencyDepartment: agency.address.departmentCode, - agencyKind: agency.kind, - agencySiret: agency.agencySiret, - agencyCounsellorEmails: agency.counsellorEmails, - agencyValidatorEmails: agency.validatorEmails, - }); }); }); describe("with ConventionJwtPayload", () => { beforeEach(() => { - uow.userRepository.users = [counsellor, validator]; uow.agencyRepository.agencies = [ toAgencyWithRights(agency, { [counsellor.id]: { @@ -366,6 +460,32 @@ describe("Get Convention", () => { role: "beneficiary", email: convention.signatories.beneficiary.email, }, + ] satisfies { role: Role; email: string }[])( + "email hash match email hash for role '$role' in convention", + async ({ role, email }: { role: Role; email: string }) => { + expectToEqual( + await getConvention.execute( + { conventionId: convention.id }, + { + role, + emailHash: makeEmailHash(email), + applicationId: convention.id, + }, + ), + { + ...convention, + agencyName: agency.name, + agencyDepartment: agency.address.departmentCode, + agencyKind: agency.kind, + agencySiret: agency.agencySiret, + agencyCounsellorEmails: [counsellor.email], + agencyValidatorEmails: [validator.email], + }, + ); + }, + ); + + it.each([ { role: "counsellor", email: counsellor.email, @@ -374,113 +494,65 @@ describe("Get Convention", () => { role: "validator", email: validator.email, }, - ] as const)( - "user '$role' has no inclusion connect", + ] satisfies { role: Role; email: string }[])( + "email hash match user email hash and has '$role' agency right", async ({ role, email }: { role: Role; email: string }) => { - const payload: ConventionJwtPayload = { - role, - emailHash: makeEmailHash(email), - applicationId: convention.id, - iat: 1, - version: 1, - }; - - const conventionResult = await getConvention.execute( - { conventionId: convention.id }, - payload, + uow.userRepository.users = [counsellor, validator]; + uow.agencyRepository.agencies = [ + toAgencyWithRights(agency, { + [validator.id]: { + isNotifiedByEmail: false, + roles: ["validator"], + }, + [counsellor.id]: { + isNotifiedByEmail: false, + roles: ["counsellor"], + }, + }), + ]; + + expectToEqual( + await getConvention.execute( + { conventionId: convention.id }, + { + role, + emailHash: makeEmailHash(email), + applicationId: convention.id, + }, + ), + { + ...convention, + agencyName: agency.name, + agencyDepartment: agency.address.departmentCode, + agencyKind: agency.kind, + agencySiret: agency.agencySiret, + agencyCounsellorEmails: [counsellor.email], + agencyValidatorEmails: [validator.email], + }, ); + }, + ); - expectToEqual(conventionResult, { - ...convention, + it("user is a FtAdvisor", async () => { + expectToEqual( + await getConvention.execute( + { conventionId: ftConnectedConvention.id }, + { + role: "validator", + emailHash: makeEmailHash(ftAdvisorEmail), + applicationId: ftConnectedConvention.id, + }, + ), + { + ...ftConnectedConvention, agencyName: agency.name, agencyDepartment: agency.address.departmentCode, agencyKind: agency.kind, agencySiret: agency.agencySiret, agencyCounsellorEmails: [counsellor.email], agencyValidatorEmails: [validator.email], - }); - }, - ); - - it("user has inclusion connect", async () => { - const inclusionConnectedUser: User = { - id: "my-user-id", - email: "john@mail.com", - firstName: "John", - lastName: "Doe", - externalId: "john-external-id", - createdAt: new Date().toISOString(), - }; - - uow.userRepository.users = [inclusionConnectedUser]; - uow.agencyRepository.agencies = [ - toAgencyWithRights(agency, { - [inclusionConnectedUser.id]: { - isNotifiedByEmail: false, - roles: ["validator"], - }, - }), - ]; - - const conventionResult = await getConvention.execute( - { conventionId: convention.id }, - { - role: "validator", - emailHash: makeEmailHash(inclusionConnectedUser.email), - applicationId: convention.id, }, ); - - expectToEqual(conventionResult, { - ...convention, - agencyName: agency.name, - agencyDepartment: agency.address.departmentCode, - agencyKind: agency.kind, - agencySiret: agency.agencySiret, - agencyCounsellorEmails: [], - agencyValidatorEmails: [inclusionConnectedUser.email], - }); - }); - - it("user is a FtAdvisor", async () => { - const ftAdvisorEmail = "ft-advisor@mail.fr"; - const ftConnectedConvention = new ConventionDtoBuilder(convention) - .withFederatedIdentity({ - provider: "peConnect", - token: "some-id", - payload: { - advisor: { - email: ftAdvisorEmail, - firstName: "john", - lastName: "doe", - type: "PLACEMENT", - }, - }, - }) - .build(); - uow.conventionRepository.setConventions([ftConnectedConvention]); - const payload: ConventionJwtPayload = { - role: "validator", - emailHash: makeEmailHash(ftAdvisorEmail), - applicationId: convention.id, - iat: 1, - version: 1, - }; - - const conventionResult = await getConvention.execute( - { conventionId: convention.id }, - payload, - ); - - expectToEqual(conventionResult, { - ...ftConnectedConvention, - agencyName: agency.name, - agencyDepartment: agency.address.departmentCode, - agencyKind: agency.kind, - agencySiret: agency.agencySiret, - agencyCounsellorEmails: [counsellor.email], - agencyValidatorEmails: [validator.email], - }); }); }); }); diff --git a/back/src/domains/establishment/ports/EstablishmentAggregateRepository.ts b/back/src/domains/establishment/ports/EstablishmentAggregateRepository.ts index 347be6c55c..235182eab5 100644 --- a/back/src/domains/establishment/ports/EstablishmentAggregateRepository.ts +++ b/back/src/domains/establishment/ports/EstablishmentAggregateRepository.ts @@ -42,7 +42,7 @@ export interface EstablishmentAggregateRepository { updatedAt: Date, ): Promise; getEstablishmentAggregateBySiret( - siret: string, + siret: SiretDto, ): Promise; getEstablishmentAggregatesByFilters( params: EstablishmentAggregateFilters, diff --git a/back/src/scripts/seed.ts b/back/src/scripts/seed.ts index 6e68ad36fd..76345887d4 100644 --- a/back/src/scripts/seed.ts +++ b/back/src/scripts/seed.ts @@ -1,50 +1,26 @@ -import { - ConventionDtoBuilder, - DiscussionBuilder, - FeatureFlags, - InclusionConnectedUserBuilder, - UserBuilder, - conventionSchema, - frontRoutes, - immersionFacileNoReplyEmailSender, - makeBooleanFeatureFlag, - makeTextImageAndRedirectFeatureFlag, - makeTextWithSeverityFeatureFlag, - reasonableSchedule, -} from "shared"; import { AppConfig } from "../config/bootstrap/appConfig"; import { AppDependencies, createAppDependencies, } from "../config/bootstrap/createAppDependencies"; -import { makeGenerateConventionMagicLinkUrl } from "../config/bootstrap/magicLinkUrl"; import { KyselyDb, makeKyselyDb } from "../config/pg/kysely/kyselyUtils"; -import { makeGenerateJwtES256 } from "../domains/core/jwt"; -import { UnitOfWork } from "../domains/core/unit-of-work/ports/UnitOfWork"; -import { UuidV4Generator } from "../domains/core/uuid-generator/adapters/UuidGeneratorImplementations"; -import { - EstablishmentAggregateBuilder, - EstablishmentEntityBuilder, - OfferEntityBuilder, -} from "../domains/establishment/helpers/EstablishmentBuilders"; -import { establishmentAggregateToFormEstablishement } from "../domains/establishment/use-cases/RetrieveFormEstablishmentFromAggregates"; -import { - getRandomAgencyId, - insertAgencies, - insertAgencySeed, -} from "./seed.helpers"; +import { agencySeed } from "./seed/agencySeed"; +import { conventionSeed } from "./seed/conventionSeed"; +import { establishmentSeed } from "./seed/establishmentSeed"; +import { featureFlagsSeed } from "./seed/featureFlagSeed"; +import { userSeed } from "./seed/userSeed"; const executeSeedTasks = async (db: KyselyDb, deps: AppDependencies) => { // biome-ignore lint/suspicious/noConsoleLog: console.log("Seed start"); - await inclusionConnectUserSeed(db); + await userSeed(db); await deps.uowPerformer.perform(async (uow) => { await featureFlagsSeed(uow); - await agencySeed(uow); + const agencyIds = await agencySeed(uow); await establishmentSeed(uow); - await conventionSeed(uow); + await conventionSeed(uow, agencyIds); }); // biome-ignore lint/suspicious/noConsoleLog: @@ -83,38 +59,6 @@ const resetDb = async (db: KyselyDb) => { console.log("Reset Db done"); }; -const icUser = new InclusionConnectedUserBuilder() - .withIsAdmin(false) - .withCreatedAt(new Date("2024-04-29")) - .withEmail("recette+playwright@immersion-facile.beta.gouv.fr") - .withFirstName("Prénom IcUser") - .withLastName("Nom IcUser") - .withId("e9dce090-f45e-46ce-9c58-4fbbb3e494ba") - .withExternalId("e9dce090-f45e-46ce-9c58-4fbbb3e494ba") - .build(); - -const adminUser = new InclusionConnectedUserBuilder() - .withIsAdmin(true) - .withCreatedAt(new Date("2024-04-30")) - .withEmail("admin+playwright@immersion-facile.beta.gouv.fr") - .withFirstName("Prénom Admin") - .withLastName("Nom Admin") - .withId("7f5cfde7-80b3-4ea1-bf3e-1711d0876161") - .withExternalId("7f5cfde7-80b3-4ea1-bf3e-1711d0876161") - .build(); - -const franceMerguezUser = new UserBuilder() - .withId("11111111-2222-4000-2222-111111111111") - .withFirstName("Daniella") - .withLastName("Velàzquez") - .withEmail("recette+merguez@immersion-facile.beta.gouv.fr") - .build(); - -const decathlonUser = new UserBuilder() - .withId("cccccccc-cccc-4000-cccc-cccccccccccc") - .withEmail("decathlon@mail.com") - .build(); - const seed = async () => { const deps = await createAppDependencies(AppConfig.createFromEnv()); const pool = deps.getPgPoolFn(); @@ -130,361 +74,6 @@ const seed = async () => { console.log("Pool end"); }; -const inclusionConnectUserSeed = async (db: KyselyDb) => { - // biome-ignore lint/suspicious/noConsoleLog: - console.log("inclusionConnectUserSeed start ..."); - - await db - .insertInto("users") - .values( - [adminUser, icUser, franceMerguezUser, decathlonUser].map((user) => ({ - id: user.id, - email: user.email, - first_name: user.firstName, - last_name: user.lastName, - inclusion_connect_sub: user.externalId, - pro_connect_sub: null, - created_at: user.createdAt, - })), - ) - .execute(); - - await db - .insertInto("users_admins") - .values({ - user_id: adminUser.id, - }) - .execute(); - - // biome-ignore lint/suspicious/noConsoleLog: - console.log("inclusionConnectUserSeed end"); -}; - -const featureFlagsSeed = async (uow: UnitOfWork) => { - // biome-ignore lint/suspicious/noConsoleLog: - console.log("featureFlagsSeed start ..."); - - const featureFlags: FeatureFlags = { - enableTemporaryOperation: makeTextImageAndRedirectFeatureFlag(false, { - message: "message", - imageUrl: "https://imageUrl", - redirectUrl: "https://redirect-url", - imageAlt: "", - title: "", - overtitle: "", - }), - enableMaintenance: makeTextWithSeverityFeatureFlag(false, { - message: "Mon message de maintenance", - severity: "warning", - }), - enableSearchByScore: makeBooleanFeatureFlag(true), - enableProConnect: makeBooleanFeatureFlag(true), - enableBroadcastOfConseilDepartementalToFT: makeBooleanFeatureFlag(false), - enableBroadcastOfCapEmploiToFT: makeBooleanFeatureFlag(false), - enableBroadcastOfMissionLocaleToFT: makeBooleanFeatureFlag(false), - }; - - await uow.featureFlagRepository.insertAll(featureFlags); - - // biome-ignore lint/suspicious/noConsoleLog: - console.log("featureFlagsSeed done"); -}; - -const agencySeed = async (uow: UnitOfWork) => { - // biome-ignore lint/suspicious/noConsoleLog: - console.log("agencySeed start ..."); - - const agenciesCountByKind = 10; - - const insertQueries = [...Array(agenciesCountByKind).keys()].flatMap(() => { - return [ - insertAgencySeed({ uow, kind: "pole-emploi", userId: adminUser.id }), - insertAgencySeed({ uow, kind: "cci", userId: adminUser.id }), - insertAgencySeed({ uow, kind: "mission-locale", userId: adminUser.id }), - insertAgencySeed({ uow, kind: "cap-emploi", userId: adminUser.id }), - insertAgencySeed({ - uow, - kind: "conseil-departemental", - userId: adminUser.id, - }), - insertAgencySeed({ - uow, - kind: "prepa-apprentissage", - userId: adminUser.id, - }), - insertAgencySeed({ uow, kind: "structure-IAE", userId: adminUser.id }), - insertAgencySeed({ uow, kind: "autre", userId: adminUser.id }), - insertAgencySeed({ uow, kind: "operateur-cep", userId: adminUser.id }), - ]; - }); - - await Promise.all([ - ...insertQueries, - insertAgencies({ uow, userId: adminUser.id }), - ]); - - // biome-ignore lint/suspicious/noConsoleLog: - console.log("agencySeed done"); -}; - -const establishmentSeed = async (uow: UnitOfWork) => { - // biome-ignore lint/suspicious/noConsoleLog: - console.log("establishmentSeed start ..."); - - const franceMerguez = new EstablishmentAggregateBuilder() - .withEstablishment( - new EstablishmentEntityBuilder() - .withLocations([ - { - id: new UuidV4Generator().new(), - address: { - city: "Villetaneuse", - postcode: "93430", - streetNumberAndAddress: "6 RUE RAYMOND BROSSE", - departmentCode: "93", - }, - position: { lat: 48.956, lon: 2.345 }, - }, - { - id: new UuidV4Generator().new(), - address: { - city: "Paris", - postcode: "75001", - streetNumberAndAddress: "1 rue de Rivoli", - departmentCode: "75", - }, - position: { lat: 48.8566, lon: 2.3522 }, - }, - ]) - .withSiret("34493368400021") - .withName("France Merguez Distribution") - .withNafDto({ code: "1013A", nomenclature: "rev2" }) // NAF Section :Industrie manufacturière - .build(), - ) - .withOffers([ - new OfferEntityBuilder() - .withAppellationCode("11569") - .withAppellationLabel("Boucher-charcutier / Bouchère-charcutière") - .withRomeCode("D1101") - .build(), - ]) - .withUserRights([ - { - userId: franceMerguezUser.id, - role: "establishment-admin", - phone: "+33600110011", - job: "Le Boss des merguez", - }, - { - userId: adminUser.id, - role: "establishment-contact", - job: "la compta", - phone: "+33672787844", - }, - ]) - .build(); - - const decathlon = new EstablishmentAggregateBuilder() - .withEstablishment( - new EstablishmentEntityBuilder() - .withSiret("50056940501696") - .withName("Decathlon france") - .withNafDto({ code: "4764Z", nomenclature: "rev2" }) // NAF Section : Commerce ; réparation d'automobiles et de motocycles - .build(), - ) - .withOffers([ - new OfferEntityBuilder() - .withAppellationCode("20552") - .withAppellationLabel("Vendeur / Vendeuse en articles de sport") - .withRomeCode("D1211") - .build(), - ]) - .withUserRights([ - { - userId: decathlonUser.id, - role: "establishment-admin", - phone: "+33600110011", - job: "The Big Boss @Decathlon", - }, - ]) - .build(); - - await uow.establishmentAggregateRepository.insertEstablishmentAggregate( - franceMerguez, - ); - await uow.establishmentAggregateRepository.insertEstablishmentAggregate( - decathlon, - ); - - await uow.groupRepository.save({ - slug: "decathlon", - sirets: [decathlon.establishment.siret], - options: { - heroHeader: { - title: "Bienvenue chez Decathlon", - description: "À fond la forme !", - }, - tintColor: "#007DBC", - }, - name: "Decathlon", - }); - - const discussionId = "aaaaaaaa-9c0a-1aaa-aa6d-aaaaaaaaaaaa"; - await uow.discussionRepository.insert( - new DiscussionBuilder() - .withId(discussionId) - .withSiret(franceMerguez.establishment.siret) - .withEstablishmentContact({ - email: "recette+playwright@immersion-facile.beta.gouv.fr", - }) - .withPotentialBeneficiary({ - resumeLink: "https://www.docdroid.net/WyjIuyO/fake-resume-pdf", - }) - .withExchanges([ - { - sender: "potentialBeneficiary", - recipient: "establishment", - sentAt: new Date("2024-02-02").toISOString(), - subject: "Présentation", - message: "Bonjour, je me présente!", - attachments: [], - }, - { - sender: "establishment", - recipient: "potentialBeneficiary", - sentAt: new Date("2024-02-03").toISOString(), - subject: "Réponse entreprise", - message: "Allez viens on est bien.", - attachments: [], - }, - ]) - .build(), - ); - - Promise.all( - [franceMerguez, decathlon].map(async (establishmentAggregate) => { - const offersAsAppellationDto = - await uow.establishmentAggregateRepository.getOffersAsAppellationAndRomeDtosBySiret( - establishmentAggregate.establishment.siret, - ); - await uow.formEstablishmentRepository.create( - await establishmentAggregateToFormEstablishement( - establishmentAggregate, - offersAsAppellationDto, - uow, - ), - ); - }), - ); - - // biome-ignore lint/suspicious/noConsoleLog: - console.log("establishmentSeed done"); -}; - -const conventionSeed = async (uow: UnitOfWork) => { - // biome-ignore lint/suspicious/noConsoleLog: - console.log("conventionSeed start..."); - - const config = AppConfig.createFromEnv(); - const generateConventionJwt = makeGenerateJwtES256<"convention">( - config.jwtPrivateKey, - 3600 * 24 * 30, - ); - const peConvention = new ConventionDtoBuilder() - .withId(new UuidV4Generator().new()) - .withInternshipKind("immersion") - .withDateStart(new Date("2023-03-27").toISOString()) - .withDateEnd(new Date("2023-03-28").toISOString()) - .withStatus("READY_TO_SIGN") - .withAgencyId(getRandomAgencyId({ kind: "pole-emploi" })) - .withSchedule(reasonableSchedule) - .build(); - - conventionSchema.parse(peConvention); - - const cciConvention = new ConventionDtoBuilder() - .withId(new UuidV4Generator().new()) - .withInternshipKind("mini-stage-cci") - .withDateStart(new Date("2023-05-01").toISOString()) - .withDateEnd(new Date("2023-05-03").toISOString()) - .withStatus("READY_TO_SIGN") - .withAgencyId(getRandomAgencyId({ kind: "cci" })) - .withSchedule(reasonableSchedule) - .build(); - - conventionSchema.parse(cciConvention); - - const conventionWithAssessmentReadyToFill = new ConventionDtoBuilder() - .withId(new UuidV4Generator().new()) - .withInternshipKind("immersion") - .withDateStart(new Date("2025-01-01").toISOString()) - .withDateEnd(new Date("2025-01-05").toISOString()) - .withStatus("ACCEPTED_BY_VALIDATOR") - .withAgencyId(getRandomAgencyId({ kind: "pole-emploi" })) - .withSchedule(reasonableSchedule) - .build(); - - conventionSchema.parse(conventionWithAssessmentReadyToFill); - - await Promise.all([ - uow.conventionRepository.save(peConvention), - uow.conventionRepository.save(cciConvention), - uow.conventionRepository.save(conventionWithAssessmentReadyToFill), - ]); - - await uow.notificationRepository.save({ - id: new UuidV4Generator().new(), - createdAt: new Date().toISOString(), - kind: "email", - templatedContent: { - kind: "ASSESSMENT_ESTABLISHMENT_NOTIFICATION", - recipients: [ - conventionWithAssessmentReadyToFill.establishmentTutor.email, - ], - sender: immersionFacileNoReplyEmailSender, - params: { - agencyLogoUrl: undefined, - beneficiaryFirstName: - conventionWithAssessmentReadyToFill.signatories.beneficiary.firstName, - beneficiaryLastName: - conventionWithAssessmentReadyToFill.signatories.beneficiary.lastName, - conventionId: conventionWithAssessmentReadyToFill.id, - establishmentTutorName: `${conventionWithAssessmentReadyToFill.establishmentTutor.firstName} ${conventionWithAssessmentReadyToFill.establishmentTutor.lastName}`, - assessmentCreationLink: makeGenerateConventionMagicLinkUrl( - config, - generateConventionJwt, - )({ - email: conventionWithAssessmentReadyToFill.establishmentTutor.email, - id: conventionWithAssessmentReadyToFill.id, - now: new Date(), - role: "establishment-tutor", - targetRoute: frontRoutes.assessment, - }), - internshipKind: conventionWithAssessmentReadyToFill.internshipKind, - }, - }, - followedIds: { - conventionId: conventionWithAssessmentReadyToFill.id, - agencyId: conventionWithAssessmentReadyToFill.agencyId, - establishmentSiret: conventionWithAssessmentReadyToFill.siret, - }, - }); - await uow.outboxRepository.save({ - id: new UuidV4Generator().new(), - occurredAt: new Date().toISOString(), - status: "published", - publications: [], - wasQuarantined: false, - topic: "EmailWithLinkToCreateAssessmentSent", - payload: { - id: conventionWithAssessmentReadyToFill.id, - }, - }); - - // biome-ignore lint/suspicious/noConsoleLog: - console.log("conventionSeed done"); -}; - seed() .then(() => { // biome-ignore lint/suspicious/noConsoleLog: diff --git a/back/src/scripts/seed/agencySeed.ts b/back/src/scripts/seed/agencySeed.ts new file mode 100644 index 0000000000..6bc1b15c55 --- /dev/null +++ b/back/src/scripts/seed/agencySeed.ts @@ -0,0 +1,138 @@ +import { AgencyId, AgencyKind } from "shared"; +import { UnitOfWork } from "../../domains/core/unit-of-work/ports/UnitOfWork"; +import { + insertAgencySeed, + insertSpecificAgenciesWithUserRight, +} from "./seed.helpers"; +import { seedUsers } from "./userSeed"; + +export const agencySeed = async ( + uow: UnitOfWork, +): Promise> => { + // biome-ignore lint/suspicious/noConsoleLog: + console.log("agencySeed start ..."); + + const agenciesCountByKind = 10; + + const randomAgencies: Record = { + "pole-emploi": await Promise.all( + [...Array(agenciesCountByKind).keys()].map(() => + insertAgencySeed({ + uow, + kind: "pole-emploi", + userId: seedUsers.adminUser.id, + }), + ), + ), + "mission-locale": await Promise.all( + [...Array(agenciesCountByKind).keys()].map(() => + insertAgencySeed({ + uow, + kind: "mission-locale", + userId: seedUsers.adminUser.id, + }), + ), + ), + "cap-emploi": await Promise.all( + [...Array(agenciesCountByKind).keys()].map(() => + insertAgencySeed({ + uow, + kind: "cap-emploi", + userId: seedUsers.adminUser.id, + }), + ), + ), + "conseil-departemental": await Promise.all( + [...Array(agenciesCountByKind).keys()].map(() => + insertAgencySeed({ + uow, + kind: "conseil-departemental", + userId: seedUsers.adminUser.id, + }), + ), + ), + "prepa-apprentissage": await Promise.all( + [...Array(agenciesCountByKind).keys()].map(() => + insertAgencySeed({ + uow, + kind: "prepa-apprentissage", + userId: seedUsers.adminUser.id, + }), + ), + ), + "structure-IAE": await Promise.all( + [...Array(agenciesCountByKind).keys()].map(() => + insertAgencySeed({ + uow, + kind: "structure-IAE", + userId: seedUsers.adminUser.id, + }), + ), + ), + cci: await Promise.all( + [...Array(agenciesCountByKind).keys()].map(() => + insertAgencySeed({ + uow, + kind: "cci", + userId: seedUsers.adminUser.id, + }), + ), + ), + cma: await Promise.all( + [...Array(agenciesCountByKind).keys()].map(() => + insertAgencySeed({ + uow, + kind: "cma", + userId: seedUsers.adminUser.id, + }), + ), + ), + "chambre-agriculture": await Promise.all( + [...Array(agenciesCountByKind).keys()].map(() => + insertAgencySeed({ + uow, + kind: "chambre-agriculture", + userId: seedUsers.adminUser.id, + }), + ), + ), + autre: await Promise.all( + [...Array(agenciesCountByKind).keys()].map(() => + insertAgencySeed({ + uow, + kind: "autre", + userId: seedUsers.adminUser.id, + }), + ), + ), + "immersion-facile": await Promise.all( + [...Array(agenciesCountByKind).keys()].map(() => + insertAgencySeed({ + uow, + kind: "immersion-facile", + userId: seedUsers.adminUser.id, + }), + ), + ), + "operateur-cep": await Promise.all( + [...Array(agenciesCountByKind).keys()].map(() => + insertAgencySeed({ + uow, + kind: "operateur-cep", + userId: seedUsers.adminUser.id, + }), + ), + ), + }; + + const randomAndSpecificAgencyIds = insertSpecificAgenciesWithUserRight({ + uow, + userId: seedUsers.adminUser.id, + agencyIds: randomAgencies, + }); + + // biome-ignore lint/suspicious/noConsoleLog: + console.log("agencySeed done"); + + return randomAndSpecificAgencyIds; +}; diff --git a/back/src/scripts/seed/conventionSeed.ts b/back/src/scripts/seed/conventionSeed.ts new file mode 100644 index 0000000000..c4e5e3655c --- /dev/null +++ b/back/src/scripts/seed/conventionSeed.ts @@ -0,0 +1,131 @@ +import { + AgencyId, + AgencyKind, + ConventionDtoBuilder, + conventionSchema, + frontRoutes, + immersionFacileNoReplyEmailSender, + reasonableSchedule, +} from "shared"; +import { AppConfig } from "../../config/bootstrap/appConfig"; +import { makeGenerateConventionMagicLinkUrl } from "../../config/bootstrap/magicLinkUrl"; +import { makeGenerateJwtES256 } from "../../domains/core/jwt"; +import { UnitOfWork } from "../../domains/core/unit-of-work/ports/UnitOfWork"; +import { UuidV4Generator } from "../../domains/core/uuid-generator/adapters/UuidGeneratorImplementations"; +import { franceMerguez } from "./establishmentSeed"; +import { getRandomAgencyId } from "./seed.helpers"; + +export const conventionSeed = async ( + uow: UnitOfWork, + agencyIds: Record, +) => { + // biome-ignore lint/suspicious/noConsoleLog: + console.log("conventionSeed start..."); + + const uuidGenerator = new UuidV4Generator(); + const peConvention = new ConventionDtoBuilder() + .withId(uuidGenerator.new()) + .withSiret(franceMerguez.establishment.siret) + .withBusinessName(franceMerguez.establishment.name) + .withInternshipKind("immersion") + .withDateStart(new Date("2023-03-27").toISOString()) + .withDateEnd(new Date("2023-03-28").toISOString()) + .withStatus("READY_TO_SIGN") + .withAgencyId(getRandomAgencyId({ kind: "pole-emploi", agencyIds })) + .withSchedule(reasonableSchedule) + .build(); + + const cciConvention = new ConventionDtoBuilder() + .withId(uuidGenerator.new()) + .withSiret(franceMerguez.establishment.siret) + .withBusinessName(franceMerguez.establishment.name) + .withInternshipKind("mini-stage-cci") + .withDateStart(new Date("2023-05-01").toISOString()) + .withDateEnd(new Date("2023-05-03").toISOString()) + .withStatus("READY_TO_SIGN") + .withAgencyId(getRandomAgencyId({ kind: "cci", agencyIds })) + .withSchedule(reasonableSchedule) + .build(); + + const conventionWithAssessmentReadyToFill = new ConventionDtoBuilder() + .withId(uuidGenerator.new()) + .withSiret(franceMerguez.establishment.siret) + .withBusinessName(franceMerguez.establishment.name) + .withInternshipKind("immersion") + .withDateStart(new Date("2025-01-01").toISOString()) + .withDateEnd(new Date("2025-01-05").toISOString()) + .withStatus("ACCEPTED_BY_VALIDATOR") + .withAgencyId(getRandomAgencyId({ kind: "pole-emploi", agencyIds })) + .withSchedule(reasonableSchedule) + .build(); + + const config = AppConfig.createFromEnv(); + const generateConventionJwt = makeGenerateJwtES256<"convention">( + config.jwtPrivateKey, + 3600 * 24 * 30, + ); + + conventionSchema.parse(peConvention); + + conventionSchema.parse(cciConvention); + + conventionSchema.parse(conventionWithAssessmentReadyToFill); + + await Promise.all([ + uow.conventionRepository.save(peConvention), + uow.conventionRepository.save(cciConvention), + uow.conventionRepository.save(conventionWithAssessmentReadyToFill), + ]); + + await uow.notificationRepository.save({ + id: uuidGenerator.new(), + createdAt: new Date().toISOString(), + kind: "email", + templatedContent: { + kind: "ASSESSMENT_ESTABLISHMENT_NOTIFICATION", + recipients: [ + conventionWithAssessmentReadyToFill.establishmentTutor.email, + ], + sender: immersionFacileNoReplyEmailSender, + params: { + agencyLogoUrl: undefined, + beneficiaryFirstName: + conventionWithAssessmentReadyToFill.signatories.beneficiary.firstName, + beneficiaryLastName: + conventionWithAssessmentReadyToFill.signatories.beneficiary.lastName, + conventionId: conventionWithAssessmentReadyToFill.id, + establishmentTutorName: `${conventionWithAssessmentReadyToFill.establishmentTutor.firstName} ${conventionWithAssessmentReadyToFill.establishmentTutor.lastName}`, + assessmentCreationLink: makeGenerateConventionMagicLinkUrl( + config, + generateConventionJwt, + )({ + email: conventionWithAssessmentReadyToFill.establishmentTutor.email, + id: conventionWithAssessmentReadyToFill.id, + now: new Date(), + role: "establishment-tutor", + targetRoute: frontRoutes.assessment, + }), + internshipKind: conventionWithAssessmentReadyToFill.internshipKind, + }, + }, + followedIds: { + conventionId: conventionWithAssessmentReadyToFill.id, + agencyId: conventionWithAssessmentReadyToFill.agencyId, + establishmentSiret: conventionWithAssessmentReadyToFill.siret, + }, + }); + await uow.outboxRepository.save({ + id: uuidGenerator.new(), + occurredAt: new Date().toISOString(), + status: "published", + publications: [], + wasQuarantined: false, + topic: "EmailWithLinkToCreateAssessmentSent", + payload: { + id: conventionWithAssessmentReadyToFill.id, + }, + }); + + // biome-ignore lint/suspicious/noConsoleLog: + console.log("conventionSeed done"); +}; diff --git a/back/src/scripts/seed/establishmentSeed.ts b/back/src/scripts/seed/establishmentSeed.ts new file mode 100644 index 0000000000..f4dbf33fa8 --- /dev/null +++ b/back/src/scripts/seed/establishmentSeed.ts @@ -0,0 +1,164 @@ +import { DiscussionBuilder } from "shared"; +import { UnitOfWork } from "../../domains/core/unit-of-work/ports/UnitOfWork"; +import { UuidV4Generator } from "../../domains/core/uuid-generator/adapters/UuidGeneratorImplementations"; +import { + EstablishmentAggregateBuilder, + EstablishmentEntityBuilder, + OfferEntityBuilder, +} from "../../domains/establishment/helpers/EstablishmentBuilders"; +import { establishmentAggregateToFormEstablishement } from "../../domains/establishment/use-cases/RetrieveFormEstablishmentFromAggregates"; +import { seedUsers } from "./userSeed"; + +export const franceMerguez = new EstablishmentAggregateBuilder() + .withEstablishment( + new EstablishmentEntityBuilder() + .withLocations([ + { + id: new UuidV4Generator().new(), + address: { + city: "Villetaneuse", + postcode: "93430", + streetNumberAndAddress: "6 RUE RAYMOND BROSSE", + departmentCode: "93", + }, + position: { lat: 48.956, lon: 2.345 }, + }, + { + id: new UuidV4Generator().new(), + address: { + city: "Paris", + postcode: "75001", + streetNumberAndAddress: "1 rue de Rivoli", + departmentCode: "75", + }, + position: { lat: 48.8566, lon: 2.3522 }, + }, + ]) + .withSiret("34493368400021") + .withName("France Merguez Distribution") + .withNafDto({ code: "1013A", nomenclature: "rev2" }) // NAF Section :Industrie manufacturière + .build(), + ) + .withOffers([ + new OfferEntityBuilder() + .withAppellationCode("11569") + .withAppellationLabel("Boucher-charcutier / Bouchère-charcutière") + .withRomeCode("D1101") + .build(), + ]) + .withUserRights([ + { + userId: seedUsers.franceMerguezUser.id, + role: "establishment-admin", + phone: "+33600110011", + job: "Le Boss des merguez", + }, + { + userId: seedUsers.adminUser.id, + role: "establishment-contact", + job: "la compta", + phone: "+33672787844", + }, + ]) + .build(); + +export const decathlon = new EstablishmentAggregateBuilder() + .withEstablishment( + new EstablishmentEntityBuilder() + .withSiret("50056940501696") + .withName("Decathlon france") + .withNafDto({ code: "4764Z", nomenclature: "rev2" }) // NAF Section : Commerce ; réparation d'automobiles et de motocycles + .build(), + ) + .withOffers([ + new OfferEntityBuilder() + .withAppellationCode("20552") + .withAppellationLabel("Vendeur / Vendeuse en articles de sport") + .withRomeCode("D1211") + .build(), + ]) + .withUserRights([ + { + userId: seedUsers.decathlonUser.id, + role: "establishment-admin", + phone: "+33600110011", + job: "The Big Boss @Decathlon", + }, + ]) + .build(); + +export const establishmentSeed = async (uow: UnitOfWork) => { + // biome-ignore lint/suspicious/noConsoleLog: + console.log("establishmentSeed start ..."); + + await uow.establishmentAggregateRepository.insertEstablishmentAggregate( + franceMerguez, + ); + await uow.establishmentAggregateRepository.insertEstablishmentAggregate( + decathlon, + ); + + await uow.groupRepository.save({ + slug: "decathlon", + sirets: [decathlon.establishment.siret], + options: { + heroHeader: { + title: "Bienvenue chez Decathlon", + description: "À fond la forme !", + }, + tintColor: "#007DBC", + }, + name: "Decathlon", + }); + + const discussionId = "aaaaaaaa-9c0a-1aaa-aa6d-aaaaaaaaaaaa"; + await uow.discussionRepository.insert( + new DiscussionBuilder() + .withId(discussionId) + .withSiret(franceMerguez.establishment.siret) + .withEstablishmentContact({ + email: "recette+playwright@immersion-facile.beta.gouv.fr", + }) + .withPotentialBeneficiary({ + resumeLink: "https://www.docdroid.net/WyjIuyO/fake-resume-pdf", + }) + .withExchanges([ + { + sender: "potentialBeneficiary", + recipient: "establishment", + sentAt: new Date("2024-02-02").toISOString(), + subject: "Présentation", + message: "Bonjour, je me présente!", + attachments: [], + }, + { + sender: "establishment", + recipient: "potentialBeneficiary", + sentAt: new Date("2024-02-03").toISOString(), + subject: "Réponse entreprise", + message: "Allez viens on est bien.", + attachments: [], + }, + ]) + .build(), + ); + + Promise.all( + [franceMerguez, decathlon].map(async (establishmentAggregate) => { + const offersAsAppellationDto = + await uow.establishmentAggregateRepository.getOffersAsAppellationAndRomeDtosBySiret( + establishmentAggregate.establishment.siret, + ); + await uow.formEstablishmentRepository.create( + await establishmentAggregateToFormEstablishement( + establishmentAggregate, + offersAsAppellationDto, + uow, + ), + ); + }), + ); + + // biome-ignore lint/suspicious/noConsoleLog: + console.log("establishmentSeed done"); +}; diff --git a/back/src/scripts/seed/featureFlagSeed.ts b/back/src/scripts/seed/featureFlagSeed.ts new file mode 100644 index 0000000000..b63c29238f --- /dev/null +++ b/back/src/scripts/seed/featureFlagSeed.ts @@ -0,0 +1,37 @@ +import { + FeatureFlags, + makeBooleanFeatureFlag, + makeTextImageAndRedirectFeatureFlag, + makeTextWithSeverityFeatureFlag, +} from "shared"; +import { UnitOfWork } from "../../domains/core/unit-of-work/ports/UnitOfWork"; + +export const featureFlagsSeed = async (uow: UnitOfWork) => { + // biome-ignore lint/suspicious/noConsoleLog: + console.log("featureFlagsSeed start ..."); + + const featureFlags: FeatureFlags = { + enableTemporaryOperation: makeTextImageAndRedirectFeatureFlag(false, { + message: "message", + imageUrl: "https://imageUrl", + redirectUrl: "https://redirect-url", + imageAlt: "", + title: "", + overtitle: "", + }), + enableMaintenance: makeTextWithSeverityFeatureFlag(false, { + message: "Mon message de maintenance", + severity: "warning", + }), + enableSearchByScore: makeBooleanFeatureFlag(true), + enableProConnect: makeBooleanFeatureFlag(true), + enableBroadcastOfConseilDepartementalToFT: makeBooleanFeatureFlag(false), + enableBroadcastOfCapEmploiToFT: makeBooleanFeatureFlag(false), + enableBroadcastOfMissionLocaleToFT: makeBooleanFeatureFlag(false), + }; + + await uow.featureFlagRepository.insertAll(featureFlags); + + // biome-ignore lint/suspicious/noConsoleLog: + console.log("featureFlagsSeed done"); +}; diff --git a/back/src/scripts/seed.helpers.ts b/back/src/scripts/seed/seed.helpers.ts similarity index 75% rename from back/src/scripts/seed.helpers.ts rename to back/src/scripts/seed/seed.helpers.ts index 9c53a2d08b..a7e24caa1e 100644 --- a/back/src/scripts/seed.helpers.ts +++ b/back/src/scripts/seed/seed.helpers.ts @@ -1,35 +1,19 @@ import { - AddressDto, AgencyDtoBuilder, AgencyId, AgencyKind, AgencyUsersRights, UserId, } from "shared"; -import { UnitOfWork } from "../domains/core/unit-of-work/ports/UnitOfWork"; -import { UuidV4Generator } from "../domains/core/uuid-generator/adapters/UuidGeneratorImplementations"; -import { toAgencyWithRights } from "../utils/agency"; -import { seedAddresses } from "./seedAddresses"; +import { UnitOfWork } from "../../domains/core/unit-of-work/ports/UnitOfWork"; +import { UuidV4Generator } from "../../domains/core/uuid-generator/adapters/UuidGeneratorImplementations"; +import { toAgencyWithRights } from "../../utils/agency"; +import { getRandomAddress, seedAddresses } from "./seedAddresses"; -let agencyIds: Record = { - "pole-emploi": [], - "mission-locale": [], - "cap-emploi": [], - "conseil-departemental": [], - "prepa-apprentissage": [], - "structure-IAE": [], - cci: [], - cma: [], - "chambre-agriculture": [], - autre: [], - "immersion-facile": [], - "operateur-cep": [], -}; - -export const getRandomAddress = (): AddressDto => - seedAddresses[Math.floor(Math.random() * seedAddresses.length)]; - -export const getRandomAgencyId = ({ kind }: { kind: AgencyKind }) => { +export const getRandomAgencyId = ({ + kind, + agencyIds, +}: { kind: AgencyKind; agencyIds: Record }) => { const ids = agencyIds[kind]; return ids[Math.floor(Math.random() * ids.length)]; }; @@ -42,7 +26,7 @@ export const insertAgencySeed = async ({ uow: UnitOfWork; kind: AgencyKind; userId: UserId; -}): Promise => { +}): Promise => { const address = getRandomAddress(); const agencyName = `Agence ${kind} ${address.city}`; const agencyId = new UuidV4Generator().new(); @@ -67,16 +51,18 @@ export const insertAgencySeed = async ({ toAgencyWithRights(agency, connectedValidator), ); - agencyIds = { - ...agencyIds, - [kind]: [...agencyIds[kind], agencyId], - }; + return agencyId; }; -export const insertAgencies = async ({ +export const insertSpecificAgenciesWithUserRight = async ({ uow, userId, -}: { uow: UnitOfWork; userId: UserId }) => { + agencyIds, +}: { + uow: UnitOfWork; + userId: UserId; + agencyIds: Record; +}): Promise> => { const peAgency = new AgencyDtoBuilder() .withId("40400c99-9c0b-bbbb-bb6d-6bb9bd300404") .withName("PE Paris") @@ -137,7 +123,7 @@ export const insertAgencies = async ({ toAgencyWithRights(missionLocaleAgency, connectedValidator), ); - agencyIds = { + return { ...agencyIds, "pole-emploi": [...agencyIds["pole-emploi"], peAgency.id], "mission-locale": [...agencyIds["mission-locale"], missionLocaleAgency.id], diff --git a/back/src/scripts/seedAddresses.ts b/back/src/scripts/seed/seedAddresses.ts similarity index 99% rename from back/src/scripts/seedAddresses.ts rename to back/src/scripts/seed/seedAddresses.ts index 6a8f590425..886a5cb185 100644 --- a/back/src/scripts/seedAddresses.ts +++ b/back/src/scripts/seed/seedAddresses.ts @@ -1,5 +1,8 @@ import { AddressDto } from "shared"; +export const getRandomAddress = (): AddressDto => + seedAddresses[Math.floor(Math.random() * seedAddresses.length)]; + export const seedAddresses: AddressDto[] = [ { city: "Paris", diff --git a/back/src/scripts/seed/userSeed.ts b/back/src/scripts/seed/userSeed.ts new file mode 100644 index 0000000000..9e31ac6560 --- /dev/null +++ b/back/src/scripts/seed/userSeed.ts @@ -0,0 +1,64 @@ +import { values } from "ramda"; +import { InclusionConnectedUserBuilder, UserBuilder } from "shared"; +import { KyselyDb } from "../../config/pg/kysely/kyselyUtils"; + +export const seedUsers = { + icUser: new InclusionConnectedUserBuilder() + .withIsAdmin(false) + .withCreatedAt(new Date("2024-04-29")) + .withEmail("recette+playwright@immersion-facile.beta.gouv.fr") + .withFirstName("Prénom IcUser") + .withLastName("Nom IcUser") + .withId("e9dce090-f45e-46ce-9c58-4fbbb3e494ba") + .withExternalId("e9dce090-f45e-46ce-9c58-4fbbb3e494ba") + .build(), + adminUser: new InclusionConnectedUserBuilder() + .withIsAdmin(true) + .withCreatedAt(new Date("2024-04-30")) + .withEmail("admin+playwright@immersion-facile.beta.gouv.fr") + .withFirstName("Prénom Admin") + .withLastName("Nom Admin") + .withId("7f5cfde7-80b3-4ea1-bf3e-1711d0876161") + .withExternalId("7f5cfde7-80b3-4ea1-bf3e-1711d0876161") + .build(), + franceMerguezUser: new UserBuilder() + .withId("11111111-2222-4000-2222-111111111111") + .withFirstName("Daniella") + .withLastName("Velàzquez") + .withEmail("recette+merguez@immersion-facile.beta.gouv.fr") + .build(), + decathlonUser: new UserBuilder() + .withId("cccccccc-cccc-4000-cccc-cccccccccccc") + .withEmail("decathlon@mail.com") + .build(), +}; + +export const userSeed = async (db: KyselyDb) => { + // biome-ignore lint/suspicious/noConsoleLog: + console.log("inclusionConnectUserSeed start ..."); + + await db + .insertInto("users") + .values( + values(seedUsers).map((user) => ({ + id: user.id, + email: user.email, + first_name: user.firstName, + last_name: user.lastName, + inclusion_connect_sub: user.externalId, + pro_connect_sub: null, + created_at: user.createdAt, + })), + ) + .execute(); + + await db + .insertInto("users_admins") + .values({ + user_id: seedUsers.adminUser.id, + }) + .execute(); + + // biome-ignore lint/suspicious/noConsoleLog: + console.log("inclusionConnectUserSeed end"); +}; diff --git a/back/src/utils/emailHash.ts b/back/src/utils/emailHash.ts index 7b6e1ef0c5..6b1c0ad150 100644 --- a/back/src/utils/emailHash.ts +++ b/back/src/utils/emailHash.ts @@ -2,10 +2,10 @@ import { toPairs } from "ramda"; import { AgencyId, AgencyWithUsersRights, - ConventionDomainPayload, ConventionDto, ConventionReadDto, Email, + EmailHash, Role, UserWithAdminRights, errors, @@ -18,11 +18,13 @@ import { agencyWithRightToAgencyDto } from "./agency"; import { conventionEmailsByRole } from "./convention"; export const isHashMatchNotNotifiedCounsellorOrValidator = async ({ - authPayload: { role, emailHash }, + role, + emailHash, agency, uow, }: { - authPayload: ConventionDomainPayload; + role: Role; + emailHash: EmailHash; agency: AgencyWithUsersRights; uow: UnitOfWork; }): Promise => { @@ -69,32 +71,34 @@ export const isHashMatchConventionEmails = async ({ convention, uow, agency, - authPayload, + emailHash, + role, }: { convention: ConventionReadDto; uow: UnitOfWork; agency: AgencyWithUsersRights; - authPayload: ConventionDomainPayload; + emailHash: EmailHash; + role: Role; }) => { const emailsByRole = conventionEmailsByRole( convention, await agencyWithRightToAgencyDto(uow, agency), - )[authPayload.role]; + )[role]; if (emailsByRole instanceof Error) throw emailsByRole; - return isSomeEmailMatchingEmailHash(emailsByRole, authPayload.emailHash); + return isSomeEmailMatchingEmailHash(emailsByRole, emailHash); }; export const isHashMatchPeAdvisorEmail = ({ convention, - authPayload, -}: { convention: ConventionReadDto; authPayload: ConventionDomainPayload }) => { + emailHash, +}: { convention: ConventionReadDto; emailHash: EmailHash }) => { const peAdvisorEmail = convention.signatories.beneficiary.federatedIdentity?.payload?.advisor .email; return peAdvisorEmail - ? isSomeEmailMatchingEmailHash([peAdvisorEmail], authPayload.emailHash) + ? isSomeEmailMatchingEmailHash([peAdvisorEmail], emailHash) : false; }; diff --git a/shared/src/errors/errors.ts b/shared/src/errors/errors.ts index 51d7e97328..313d744b62 100644 --- a/shared/src/errors/errors.ts +++ b/shared/src/errors/errors.ts @@ -129,9 +129,12 @@ export const errors = { ), forbiddenMissingRights: ({ conventionId, - }: { conventionId: ConventionId }) => + userId, + }: { conventionId: ConventionId; userId?: UserId }) => new ForbiddenError( - `L'utilisateur n'a pas de droits sur la convention '${conventionId}'.`, + `L'utilisateur ${ + userId ? `'${userId}' ` : "" + }n'a pas de droits sur la convention '${conventionId}'.`, ), badRoleStatusChange: ({ roles, diff --git a/shared/src/tokens/jwt.utils.ts b/shared/src/tokens/jwt.utils.ts index c27ba894e0..de3f43f58d 100644 --- a/shared/src/tokens/jwt.utils.ts +++ b/shared/src/tokens/jwt.utils.ts @@ -4,12 +4,13 @@ import { Email, SiretDto, currentJwtVersions } from ".."; import { ConventionJwtPayload, CreateConventionMagicLinkPayloadProperties, + EmailHash, EstablishmentJwtPayload, } from "./jwtPayload.dto"; export const isSomeEmailMatchingEmailHash = ( emailsOrError: Email[], - emailHash: string, + emailHash: EmailHash, ): boolean => emailsOrError.some((email) => makeEmailHash(email) === emailHash); export const makeEmailHash = (email: Email): string => stringToMd5(email); diff --git a/shared/src/tokens/jwtPayload.dto.ts b/shared/src/tokens/jwtPayload.dto.ts index 5685d8ae3b..33046d2fc8 100644 --- a/shared/src/tokens/jwtPayload.dto.ts +++ b/shared/src/tokens/jwtPayload.dto.ts @@ -5,6 +5,7 @@ import { } from "../inclusionConnectedAllowed/inclusionConnectedAllowed.dto"; import { Role } from "../role/role.dto"; import { SiretDto } from "../siret/siret"; +import { Flavor } from "../typeFlavors"; import { ValueOf } from "../utils"; export type CommonJwtPayload = { @@ -18,10 +19,12 @@ export type StandardJwtPayload = { role: R; }; +export type EmailHash = Flavor; + export type ConventionDomainPayload = { applicationId: ConventionId; role: Role; - emailHash: string; //< md5 of email + emailHash: EmailHash; //< md5 of email sub?: string; }; export type ConventionJwtPayload = CommonJwtPayload & ConventionDomainPayload;