Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#2825 - ETQ conseiller ou validateur notifié, je peux créer ou vérifier l'existence du bilan à partir d'un lien magique #2831

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
expectToEqual,
expiredMagicLinkErrorMessage,
frontRoutes,
stringToMd5,
makeEmailHash,
technicalRoutes,
unauthenticatedConventionRoutes,
} from "shared";
Expand Down Expand Up @@ -282,7 +282,9 @@ describe("convention e2e", () => {
generateConventionJwt({
applicationId: convention.id,
role: "beneficiary",
emailHash: stringToMd5(convention.signatories.beneficiary.email),
emailHash: makeEmailHash(
convention.signatories.beneficiary.email,
),
iat: Math.round(gateways.timeGateway.now().getTime() / 1000),
exp:
Math.round(gateways.timeGateway.now().getTime() / 1000) +
Expand Down
41 changes: 21 additions & 20 deletions back/src/adapters/primary/routers/magicLink/assessment.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
conventionMagicLinkRoutes,
createConventionMagicLinkPayload,
displayRouteName,
errors,
expectHttpResponseToEqual,
} from "shared";
import { HttpClient } from "shared-routes";
Expand All @@ -16,9 +17,13 @@ import { InMemoryUnitOfWork } from "../../../../domains/core/unit-of-work/adapte
import { InMemoryGateways, buildTestApp } from "../../../../utils/buildTestApp";
import { processEventsForEmailToBeSent } from "../../../../utils/processEventsForEmailToBeSent";

const conventionId = "my-Convention-id";

describe("Assessment routes", () => {
const agency = new AgencyDtoBuilder().build();
const convention = new ConventionDtoBuilder()
.withAgencyId(agency.id)
.withStatus("ACCEPTED_BY_VALIDATOR")
.build();

let httpClient: HttpClient<ConventionMagicLinkRoutes>;
let inMemoryUow: InMemoryUnitOfWork;
let eventCrawler: BasicEventCrawler;
Expand All @@ -31,34 +36,25 @@ describe("Assessment routes", () => {

jwt = testAppAndDeps.generateConventionJwt(
createConventionMagicLinkPayload({
id: conventionId,
id: convention.id,
role: "establishment-tutor",
email: "[email protected]",
email: convention.establishmentTutor.email,
now: new Date(),
}),
);
httpClient = createSupertestSharedClient(
conventionMagicLinkRoutes,
testAppAndDeps.request,
);
inMemoryUow.conventionRepository.setConventions([convention]);
});

describe(`${displayRouteName(
conventionMagicLinkRoutes.createAssessment,
)} to add assessment`, () => {
it("returns 201 if the jwt is valid", async () => {
const agency = new AgencyDtoBuilder().build();

const convention = new ConventionDtoBuilder()
.withId(conventionId)
.withAgencyId(agency.id)
.withStatus("ACCEPTED_BY_VALIDATOR")
.build();

inMemoryUow.conventionRepository.setConventions([convention]);

const assessment: AssessmentDto = {
conventionId,
conventionId: convention.id,
status: "COMPLETED",
establishmentFeedback: "The guy left after one day",
endedWithAJob: false,
Expand Down Expand Up @@ -87,7 +83,7 @@ describe("Assessment routes", () => {

it("fails with 401 if jwt is not valid", async () => {
const assessment: AssessmentDto = {
conventionId,
conventionId: convention.id,
status: "COMPLETED",
establishmentFeedback: "The guy left after one day",
endedWithAJob: false,
Expand All @@ -107,7 +103,7 @@ describe("Assessment routes", () => {

it("fails with 400 if some data is not valid", async () => {
const assessment: AssessmentDto = {
conventionId,
conventionId: convention.id,
status: "COMPLETED",
establishmentFeedback: "",
endedWithAJob: false,
Expand All @@ -131,8 +127,14 @@ describe("Assessment routes", () => {
});

it("fails with 403 if convention id does not matches the one in token", async () => {
const anotherConvention = new ConventionDtoBuilder()
.withId("another-convention-id")
.build();

inMemoryUow.conventionRepository.setConventions([anotherConvention]);

const assessment: AssessmentDto = {
conventionId: "another-convention-id",
conventionId: anotherConvention.id,
status: "COMPLETED",
establishmentFeedback: "mon feedback",
endedWithAJob: false,
Expand All @@ -148,8 +150,7 @@ describe("Assessment routes", () => {
status: 403,
body: {
status: 403,
message:
"Convention provided in DTO is not the same as application linked to it",
message: errors.assessment.conventionIdMismatch().message,
},
});
});
Expand Down
16 changes: 9 additions & 7 deletions back/src/domains/convention/entities/AssessmentEntity.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import { AssessmentDto, ConventionDto, ConventionStatus } from "shared";
import { BadRequestError } from "shared";
import { AssessmentDto, ConventionDto, ConventionStatus, errors } from "shared";
import { EntityFromDto } from "../../../utils/EntityFromDto";

export type AssessmentEntity = EntityFromDto<AssessmentDto, "Assessment">;

const acceptedConventionStatuses: ConventionStatus[] = [
export const acceptedConventionStatusesForAssessment: ConventionStatus[] = [
"ACCEPTED_BY_VALIDATOR",
];

export const createAssessmentEntity = (
dto: AssessmentDto,
convention: ConventionDto,
): AssessmentEntity => {
if (!acceptedConventionStatuses.includes(convention.status))
throw new BadRequestError(
`Cannot create an assessment for which the convention has not been validated, status was ${convention.status}`,
);
if (!acceptedConventionStatusesForAssessment.includes(convention.status))
throw errors.assessment.badStatus(convention.status);

return {
_entityName: "Assessment",
...dto,
};
};

export const toAssessmentDto = ({
_entityName,
...assessmentEntity
}: AssessmentEntity): AssessmentDto => assessmentEntity;
16 changes: 8 additions & 8 deletions back/src/domains/convention/entities/Convention.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
AgencyWithUsersRights,
ApiConsumer,
ConventionDto,
ConventionId,
ConventionReadDto,
ConventionStatus,
ForbiddenError,
Expand Down Expand Up @@ -57,19 +58,18 @@ const oneOfTheRolesIsAllowed = ({
allowedRoles.includes(roleToValidate),
);

export async function retrieveConventionWithAgency(
export const retrieveConventionWithAgency = async (
uow: UnitOfWork,
conventionInPayload: ConventionDto,
conventionId: ConventionId,
): Promise<{
agency: AgencyWithUsersRights;
convention: ConventionReadDto;
}> {
const convention = await uow.conventionQueries.getConventionById(
conventionInPayload.id,
);
}> => {
const convention =
await uow.conventionQueries.getConventionById(conventionId);
if (!convention)
throw errors.convention.notFound({
conventionId: conventionInPayload.id,
conventionId,
});
const agency = await uow.agencyRepository.getById(convention.agencyId);
if (!agency) throw errors.agency.notFound({ agencyId: convention.agencyId });
Expand All @@ -78,7 +78,7 @@ export async function retrieveConventionWithAgency(
agency,
convention,
};
}
};

export const getAllConventionRecipientsEmail = (
convention: ConventionDto,
Expand Down
80 changes: 45 additions & 35 deletions back/src/domains/convention/use-cases/CreateAssessment.ts
Original file line number Diff line number Diff line change
@@ -1,76 +1,86 @@
import { AssessmentDto, ConventionJwtPayload, assessmentSchema } from "shared";
import { ConflictError, ForbiddenError, NotFoundError } from "shared";
import { throwForbiddenIfNotAllow } from "../../../utils/assessment";
import {
AssessmentDto,
ConventionDomainPayload,
ConventionDto,
ForbiddenError,
assessmentSchema,
errors,
} from "shared";
import { agencyWithRightToAgencyDto } from "../../../utils/agency";
import { throwForbiddenIfNotAllowedForAssessments } from "../../../utils/assessment";
import { createTransactionalUseCase } from "../../core/UseCase";
import { CreateNewEvent } from "../../core/events/ports/EventBus";
import { UnitOfWork } from "../../core/unit-of-work/ports/UnitOfWork";
import {
AssessmentEntity,
createAssessmentEntity,
} from "../entities/AssessmentEntity";
import { retrieveConventionWithAgency } from "../entities/Convention";

type WithCreateNewEvent = { createNewEvent: CreateNewEvent };

export type CreateAssessment = ReturnType<typeof makeCreateAssessment>;
export const makeCreateAssessment = createTransactionalUseCase<
AssessmentDto,
void,
ConventionJwtPayload | undefined,
ConventionDomainPayload | undefined,
WithCreateNewEvent
>(
{ name: "CreateAssessment", inputSchema: assessmentSchema },
async ({
inputParams: dto,
inputParams: assessment,
uow,
deps,
currentUser: conventionJwtPayload,
}) => {
if (!conventionJwtPayload)
throw new ForbiddenError("No magic link provided");
throwForbiddenIfNotAllow(dto.conventionId, conventionJwtPayload);

const assessmentEntity = await validateConventionAndCreateAssessmentEntity(
const { agency, convention } = await retrieveConventionWithAgency(
uow,
dto,
assessment.conventionId,
);

const event = deps.createNewEvent({
topic: "AssessmentCreated",
payload: {
assessment: dto,
triggeredBy: {
kind: "convention-magic-link",
role: conventionJwtPayload.role,
},
},
});
throwForbiddenIfNotAllowedForAssessments(
convention,
await agencyWithRightToAgencyDto(uow, agency),
conventionJwtPayload,
);

const assessmentEntity = await validateConventionAndCreateAssessmentEntity(
uow,
convention,
assessment,
);

await Promise.all([
uow.assessmentRepository.save(assessmentEntity),
uow.outboxRepository.save(event),
uow.outboxRepository.save(
deps.createNewEvent({
topic: "AssessmentCreated",
payload: {
assessment: assessment,
triggeredBy: {
kind: "convention-magic-link",
role: conventionJwtPayload.role,
},
},
}),
),
]);
},
);

const validateConventionAndCreateAssessmentEntity = async (
uow: UnitOfWork,
dto: AssessmentDto,
convention: ConventionDto,
assessment: AssessmentDto,
): Promise<AssessmentEntity> => {
const conventionId = dto.conventionId;
const convention = await uow.conventionRepository.getById(conventionId);

if (!convention)
throw new NotFoundError(
`Did not found convention with id: ${conventionId}`,
);

const assessment =
await uow.assessmentRepository.getByConventionId(conventionId);
const existingAssessmentEntity =
await uow.assessmentRepository.getByConventionId(convention.id);

if (assessment)
throw new ConflictError(
`Cannot create an assessment as one already exists for convention with id : ${conventionId}`,
);
if (existingAssessmentEntity)
throw errors.assessment.alreadyExist(convention.id);

return createAssessmentEntity(dto, convention);
return createAssessmentEntity(assessment, convention);
};
Loading
Loading