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

118 ajouter un statut pour les immersions qui deviennent obsolètes car en doublon dune convention déjà créée #252

Merged
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
21 changes: 21 additions & 0 deletions back/src/_testBuilders/emailAssertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,24 @@ export const expectNotifyBeneficiaryAndEnterpriseThatApplicationIsRejected = (
},
});
};

export const expectNotifyBeneficiaryAndEnterpriseThatConventionIsDeprecated = (
templatedEmail: TemplatedEmail,
recipients: string[],
convention: ConventionDto,
) => {
expectToEqual(templatedEmail, {
type: "DEPRECATED_CONVENTION_NOTIFICATION",
recipients,
params: {
internshipKind: convention.internshipKind,
beneficiaryFirstName: convention.signatories.beneficiary.firstName,
beneficiaryLastName: convention.signatories.beneficiary.lastName,
businessName: convention.businessName,
deprecationReason: convention.statusJustification || "",
immersionProfession: convention.immersionAppellation.appellationLabel,
dateEnd: convention.dateEnd,
dateStart: convention.dateStart,
},
});
};
6 changes: 6 additions & 0 deletions back/src/adapters/primary/config/createUseCases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { GetConvention } from "../../../domain/convention/useCases/GetConvention
import { ConfirmToSignatoriesThatApplicationCorrectlySubmittedRequestSignature } from "../../../domain/convention/useCases/notifications/ConfirmToSignatoriesThatApplicationCorrectlySubmittedRequestSignature";
import { DeliverRenewedMagicLink } from "../../../domain/convention/useCases/notifications/DeliverRenewedMagicLink";
import { NotifyAllActorsOfFinalConventionValidation } from "../../../domain/convention/useCases/notifications/NotifyAllActorsOfFinalConventionValidation";
import { NotifyAllActorsThatConventionIsDeprecated } from "../../../domain/convention/useCases/notifications/NotifyAllActorsThatConventionIsDeprecated";
import { NotifyBeneficiaryAndEnterpriseThatApplicationIsRejected } from "../../../domain/convention/useCases/notifications/NotifyBeneficiaryAndEnterpriseThatApplicationIsRejected";
import { NotifyBeneficiaryAndEnterpriseThatApplicationNeedsModification } from "../../../domain/convention/useCases/notifications/NotifyBeneficiaryAndEnterpriseThatApplicationNeedsModification";
import { NotifyConventionReminder } from "../../../domain/convention/useCases/notifications/NotifyConventionReminder";
Expand Down Expand Up @@ -365,6 +366,11 @@ export const createUseCases = (
uowPerformer,
saveNotificationAndRelatedEvent,
),
notifyAllActorsThatConventionIsDeprecated:
new NotifyAllActorsThatConventionIsDeprecated(
uowPerformer,
saveNotificationAndRelatedEvent,
),
notifyBeneficiaryAndEnterpriseThatConventionNeedsModifications:
new NotifyBeneficiaryAndEnterpriseThatApplicationNeedsModification(
uowPerformer,
Expand Down
1 change: 1 addition & 0 deletions back/src/adapters/primary/subscribeToEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const getUseCasesByTopics = (
ImmersionApplicationCancelled: [
useCases.broadcastToPoleEmploiOnConventionUpdates,
],
ConventionDeprecated: [useCases.notifyAllActorsThatConventionIsDeprecated],
ConventionReminderRequired: [useCases.notifyConventionReminder],

// Establishment form related
Expand Down
1 change: 1 addition & 0 deletions back/src/domain/convention/ports/PoleEmploiGateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const conventionStatusToPoleEmploiStatus = {
// si rejeté
REJECTED: "REJETÉ",
CANCELLED: "DEMANDE_ANNULEE",
DEPRECATED: "DEMANDE_OBSOLETE",

// // à venir potentiellement
// ABANDONNED: "ABANDONNÉ",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type ConventionDomainTopic = ExtractFromDomainTopics<
| "ImmersionApplicationRejected"
| "ImmersionApplicationRequiresModification"
| "ImmersionApplicationCancelled"
| "ConventionDeprecated"
> | null; // null is used to indicate that no domain event should be sent

type SetupInitialStateParams = {
Expand Down
9 changes: 7 additions & 2 deletions back/src/domain/convention/useCases/UpdateConventionStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const domainTopicByTargetStatusMap: Record<
REJECTED: "ImmersionApplicationRejected",
CANCELLED: "ImmersionApplicationCancelled",
DRAFT: "ImmersionApplicationRequiresModification",
DEPRECATED: "ConventionDeprecated",
};

type UpdateConventionStatusPayload = {
Expand Down Expand Up @@ -76,7 +77,9 @@ export class UpdateConventionStatus extends TransactionalUseCase<
: undefined,
)
.withStatusJustification(
status === "CANCELLED" || status === "REJECTED"
status === "CANCELLED" ||
status === "REJECTED" ||
status === "DEPRECATED"
? params.statusJustification
: undefined,
);
Expand All @@ -95,7 +98,9 @@ export class UpdateConventionStatus extends TransactionalUseCase<
updatedDto,
domainTopic,
role,
params.status === "REJECTED" || params.status === "DRAFT"
params.status === "REJECTED" ||
params.status === "DRAFT" ||
params.status === "DEPRECATED"
? params.statusJustification
clement-duport marked this conversation as resolved.
Show resolved Hide resolved
: undefined,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,25 @@ describe("UpdateConventionStatus", () => {
});
});

describe("* -> DEPRECATED transition", () => {
testForAllRolesAndInitialStatusCases({
updateStatusParams: {
status: "DEPRECATED",
statusJustification: "my deprecation justification",
},
expectedDomainTopic: "ConventionDeprecated",
updatedFields: { statusJustification: "my deprecation justification" },
allowedRoles: ["backOffice", "validator", "counsellor"],
allowedInitialStatuses: [
"PARTIALLY_SIGNED",
"READY_TO_SIGN",
"IN_REVIEW",
"ACCEPTED_BY_COUNSELLOR",
"DRAFT",
],
});
});

it("fails for unknown application ids", async () => {
const { updateConventionStatus, conventionRepository } =
await setupInitialState({ initialStatus: "IN_REVIEW" });
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { uniq } from "ramda";
import { ConventionDto, conventionSchema } from "shared";
import {
UnitOfWork,
UnitOfWorkPerformer,
} from "../../../core/ports/UnitOfWork";
import { TransactionalUseCase } from "../../../core/UseCase";
import { SaveNotificationAndRelatedEvent } from "../../../generic/notifications/entities/Notification";

export class NotifyAllActorsThatConventionIsDeprecated extends TransactionalUseCase<ConventionDto> {
constructor(
uowPerformer: UnitOfWorkPerformer,
private readonly saveNotificationAndRelatedEvent: SaveNotificationAndRelatedEvent,
) {
super(uowPerformer);
}

inputSchema = conventionSchema;

public async _execute(
convention: ConventionDto,
uow: UnitOfWork,
): Promise<void> {
const [agency] = await uow.agencyRepository.getByIds([convention.agencyId]);
if (!agency) {
throw new Error(
`Unable to send mail. No agency config found for ${convention.agencyId}`,
);
}

const {
beneficiary,
establishmentRepresentative,
beneficiaryCurrentEmployer,
beneficiaryRepresentative,
} = convention.signatories;

const recipients = uniq([
beneficiary.email,
establishmentRepresentative.email,
...agency.counsellorEmails,
...agency.validatorEmails,
...(beneficiaryCurrentEmployer ? [beneficiaryCurrentEmployer.email] : []),
...(beneficiaryRepresentative ? [beneficiaryRepresentative.email] : []),
]);

await this.saveNotificationAndRelatedEvent(uow, {
kind: "email",
templatedContent: {
type: "DEPRECATED_CONVENTION_NOTIFICATION",
recipients,
params: {
internshipKind: convention.internshipKind,
beneficiaryFirstName: beneficiary.firstName,
beneficiaryLastName: beneficiary.lastName,
businessName: convention.businessName,
deprecationReason: convention.statusJustification || "",
dateStart: convention.dateStart,
dateEnd: convention.dateEnd,
immersionProfession: convention.immersionAppellation.appellationLabel,
},
},
followedIds: {
conventionId: convention.id,
agencyId: convention.agencyId,
establishmentSiret: convention.siret,
},
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {
AgencyDto,
AgencyDtoBuilder,
BeneficiaryCurrentEmployer,
BeneficiaryRepresentative,
ConventionDtoBuilder,
} from "shared";
import { expectNotifyBeneficiaryAndEnterpriseThatConventionIsDeprecated } from "../../../../_testBuilders/emailAssertions";
import {
createInMemoryUow,
InMemoryUnitOfWork,
} from "../../../../adapters/primary/config/uowConfig";
import { CustomTimeGateway } from "../../../../adapters/secondary/core/TimeGateway/CustomTimeGateway";
import { UuidV4Generator } from "../../../../adapters/secondary/core/UuidGeneratorImplementations";
import { InMemoryUowPerformer } from "../../../../adapters/secondary/InMemoryUowPerformer";
import { makeCreateNewEvent } from "../../../core/eventBus/EventBus";
import {
EmailNotification,
makeSaveNotificationAndRelatedEvent,
} from "../../../generic/notifications/entities/Notification";
import { NotifyAllActorsThatConventionIsDeprecated } from "./NotifyAllActorsThatConventionIsDeprecated";

const beneficiaryRepresentative: BeneficiaryRepresentative = {
role: "beneficiary-representative",
email: "legal@representative.com",
firstName: "The",
lastName: "Representative",
phone: "1234567",
};

const beneficiaryCurrentEmployer: BeneficiaryCurrentEmployer = {
firstName: "ali",
lastName: "baba",
businessName: "business",
businessSiret: "01234567890123",
email: "beneficiary-current-employer@gmail.com",
job: "job",
phone: "0011223344",
role: "beneficiary-current-employer",
signedAt: new Date().toISOString(),
businessAddress: "Rue des Bouchers 67065 Strasbourg",
};

const deprecatedConvention = new ConventionDtoBuilder()
.withStatus("DEPRECATED")
.withStatusJustification("test-deprecation-justification")
.withBeneficiaryRepresentative(beneficiaryRepresentative)
.withBeneficiaryCurrentEmployer(beneficiaryCurrentEmployer)
.build();

const counsellorEmails = ["counsellor1@email.fr", "counsellor2@email.fr"];

const validatorEmails = ["validator@gmail.com"];

const defaultAgency = AgencyDtoBuilder.create(deprecatedConvention.agencyId)
.withName("test-agency-name")
.withCounsellorEmails(counsellorEmails)
.withValidatorEmails(validatorEmails)
.build();

describe("NotifyAllActorsThatApplicationIsDeprecated", () => {
let useCase: NotifyAllActorsThatConventionIsDeprecated;
let agency: AgencyDto;
let uow: InMemoryUnitOfWork;

beforeEach(() => {
agency = defaultAgency;
uow = createInMemoryUow();
uow.agencyRepository.setAgencies([agency]);

const timeGateway = new CustomTimeGateway();
const uuidGenerator = new UuidV4Generator();
const createNewEvent = makeCreateNewEvent({ uuidGenerator, timeGateway });
const saveNotificationAndRelatedEvent = makeSaveNotificationAndRelatedEvent(
createNewEvent,
uuidGenerator,
timeGateway,
);
useCase = new NotifyAllActorsThatConventionIsDeprecated(
new InMemoryUowPerformer(uow),
saveNotificationAndRelatedEvent,
);
});

it("Sends a conevention deprecated notification to all actors", async () => {
await useCase.execute(deprecatedConvention);

const templatedEmailsSent = uow.notificationRepository.notifications
.filter((notif): notif is EmailNotification => notif.kind === "email")
.map((notif) => notif.templatedContent);

expect(templatedEmailsSent).toHaveLength(1);

expectNotifyBeneficiaryAndEnterpriseThatConventionIsDeprecated(
templatedEmailsSent[0],
[
deprecatedConvention.signatories.beneficiary.email,
deprecatedConvention.signatories.establishmentRepresentative.email,
...counsellorEmails,
...validatorEmails,
deprecatedConvention.signatories.beneficiaryCurrentEmployer!.email,
deprecatedConvention.signatories.beneficiaryRepresentative!.email,
],
deprecatedConvention,
);
});
});
1 change: 1 addition & 0 deletions back/src/domain/core/eventBus/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export type DomainEvent =
| GenericEvent<"ImmersionApplicationRejected", ConventionDto>
| GenericEvent<"ImmersionApplicationCancelled", ConventionDto>
| GenericEvent<"ImmersionApplicationRequiresModification", ConventionRequiresModificationPayload>
| GenericEvent<"ConventionDeprecated", ConventionDto>

// MAGIC LINK RENEWAL
| GenericEvent<"MagicLinkRenewalRequested", RenewMagicLinkPayload>
Expand Down
10 changes: 10 additions & 0 deletions front/src/app/components/admin/ConventionManageActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ export const ConventionManageActions = ({
</VerificationActionButton>
)}

{isAllowedTransition(convention.status, "DEPRECATED", role) && (
<VerificationActionButton
disabled={disabled}
newStatus="DEPRECATED"
onSubmit={createOnSubmitWithFeedbackKind("deprecated")}
>
{t.verification.markAsDeprecated}
</VerificationActionButton>
)}

{isAllowedTransition(convention.status, "DRAFT", role) && (
<VerificationActionButton
disabled={disabled}
Expand Down
1 change: 1 addition & 0 deletions front/src/app/components/admin/ConventionValidation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const labelByStatus: Record<ConventionStatus, string> = {
PARTIALLY_SIGNED: "[✍️ Partiellement signée]",
READY_TO_SIGN: "[📄 En cours de signature]",
REJECTED: "[❌ DEMANDE REJETÉE]",
DEPRECATED: "[❌ DEMANDE OBSOLÈTE]",
};

export interface ConventionValidationProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export const createConventionFeedbackMessageByKind = (
</InitialSubmitSuccessMessageBase>
),
cancelled: "Succès. La convention a bien été annulée.",
deprecated:
"Succès. La convention a bien été supprimé. La confirmation de cette suppression va être communiquée par mail à chacun des signataires.",
});

const InitialSubmitSuccessMessageBase = ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ const { CancelModal, openCancelModal, closeCancelModal } = createModal({
isOpenedByDefault: false,
});

const { DeprecateModal, openDeprecateModal, closeDeprecateModal } = createModal(
{
name: "deprecate",
isOpenedByDefault: false,
},
);

const ModalByStatus = (status: VerificationActionsModal) => {
const modals = {
DRAFT: {
Expand All @@ -67,6 +74,11 @@ const ModalByStatus = (status: VerificationActionsModal) => {
openModal: openCancelModal,
closeModal: closeCancelModal,
},
DEPRECATED: {
modal: DeprecateModal,
openModal: openDeprecateModal,
closeModal: closeDeprecateModal,
},
};
return modals[status];
};
Expand All @@ -91,6 +103,8 @@ export const VerificationActionButton = ({
ACCEPTED_BY_COUNSELLOR:
domElementIds.manageConvention.conventionValidationValidateButton,
CANCELLED: domElementIds.manageConvention.conventionValidationCancelButton,
DEPRECATED:
domElementIds.manageConvention.conventionValidationDeprecateButton,
};

return (
Expand Down Expand Up @@ -249,11 +263,13 @@ const JustificationModalContent = ({
const inputLabelByStatus: Record<ConventionStatusWithJustification, string> = {
DRAFT: "Précisez la raison et la modification nécessaire",
REJECTED: "Pourquoi l'immersion est-elle refusée ?",
CANCELLED: "Pourquoi souhaitez-vous annuler cette convention?",
CANCELLED: "Pourquoi souhaitez-vous annuler cette convention ?",
DEPRECATED: "Pourquoi l'immersion est-elle obsolète ?",
};

const confirmByStatus: Record<ConventionStatusWithJustification, string> = {
DRAFT: "Confirmer la demande de modification",
REJECTED: "Confirmer le refus",
CANCELLED: "Confirmer l'annulation",
DEPRECATED: "Confirmer que la demande est obsolète",
};
Loading