From ceb846d64ec438e74e89d9dae28a88ffac0191f8 Mon Sep 17 00:00:00 2001 From: Jonathan Perrault Date: Tue, 16 Jan 2024 14:26:09 +0100 Subject: [PATCH] feat: prepare load testing --- packages/app/.env.development | 1 - packages/app/.env.production | 1 - packages/app/.env.test | 2 - packages/app/sentry.client.config.ts | 2 +- packages/app/sentry.edge.config.ts | 2 +- packages/app/sentry.server.config.ts | 2 +- .../src/app/(default)/load-test/IndexList.tsx | 138 +++++++++++++++ .../src/app/(default)/load-test/RepeqList.tsx | 62 +++++++ .../app/src/app/(default)/load-test/page.tsx | 80 +++++++++ .../app/src/app/api/public/load-test/route.ts | 165 ++++++++++++++++++ 10 files changed, 448 insertions(+), 7 deletions(-) create mode 100644 packages/app/src/app/(default)/load-test/IndexList.tsx create mode 100644 packages/app/src/app/(default)/load-test/RepeqList.tsx create mode 100644 packages/app/src/app/(default)/load-test/page.tsx create mode 100644 packages/app/src/app/api/public/load-test/route.ts diff --git a/packages/app/.env.development b/packages/app/.env.development index 6376af555..6838b1c02 100644 --- a/packages/app/.env.development +++ b/packages/app/.env.development @@ -81,5 +81,4 @@ SECURITY_CHARON_URL="https://egapro-charon.dev.fabrique.social.gouv.fr" # old EGAPRO_SENTRY_DSN SENTRY_DSN="" # old EGAPRO_FLAVOUR -SENTRY_ENVIRONMENT="development" SENTRY_AUTH_TOKEN=3b4a6e7f1e2346cebd6ad7fa390d66c800dfce8331fc4982a12aafefed1cc47f diff --git a/packages/app/.env.production b/packages/app/.env.production index b37e433f1..a389acd60 100644 --- a/packages/app/.env.production +++ b/packages/app/.env.production @@ -75,5 +75,4 @@ SECURITY_CHARON_URL="https://egapro-charon.dev.fabrique.social.gouv.fr" # old EGAPRO_SENTRY_DSN SENTRY_DSN="" # old EGAPRO_FLAVOUR -SENTRY_ENVIRONMENT="prod" SENTRY_AUTH_TOKEN=3b4a6e7f1e2346cebd6ad7fa390d66c800dfce8331fc4982a12aafefed1cc47f diff --git a/packages/app/.env.test b/packages/app/.env.test index ef588225d..ccfc2b349 100644 --- a/packages/app/.env.test +++ b/packages/app/.env.test @@ -66,5 +66,3 @@ SECURITY_ALLOWED_IPS="" # old EGAPRO_SENTRY_DSN SENTRY_DSN="" # old EGAPRO_FLAVOUR -SENTRY_ENVIRONMENT="test" - diff --git a/packages/app/sentry.client.config.ts b/packages/app/sentry.client.config.ts index cb63b2645..8231d43c9 100644 --- a/packages/app/sentry.client.config.ts +++ b/packages/app/sentry.client.config.ts @@ -6,7 +6,7 @@ import * as Sentry from "@sentry/nextjs"; Sentry.init({ dsn: "https://28b6186c058a49fc94ee665667e44612@sentry.fabrique.social.gouv.fr/99", - environment: process.env.SENTRY_ENVIRONMENT, + environment: process.env.EGAPRO_ENV || "dev", // Adjust this value in production, or use tracesSampler for greater control tracesSampleRate: 1, diff --git a/packages/app/sentry.edge.config.ts b/packages/app/sentry.edge.config.ts index d82002bd3..c2237ebf2 100644 --- a/packages/app/sentry.edge.config.ts +++ b/packages/app/sentry.edge.config.ts @@ -7,7 +7,7 @@ import * as Sentry from "@sentry/nextjs"; Sentry.init({ dsn: "https://28b6186c058a49fc94ee665667e44612@sentry.fabrique.social.gouv.fr/99", - environment: process.env.SENTRY_ENVIRONMENT, + environment: process.env.EGAPRO_ENV || "dev", // Adjust this value in production, or use tracesSampler for greater control tracesSampleRate: 1, diff --git a/packages/app/sentry.server.config.ts b/packages/app/sentry.server.config.ts index 76e48ae34..b95a4b618 100644 --- a/packages/app/sentry.server.config.ts +++ b/packages/app/sentry.server.config.ts @@ -6,7 +6,7 @@ import * as Sentry from "@sentry/nextjs"; Sentry.init({ dsn: "https://28b6186c058a49fc94ee665667e44612@sentry.fabrique.social.gouv.fr/99", - environment: process.env.SENTRY_ENVIRONMENT, + environment: process.env.EGAPRO_ENV || "dev", // Adjust this value in production, or use tracesSampler for greater control tracesSampleRate: 1, diff --git a/packages/app/src/app/(default)/load-test/IndexList.tsx b/packages/app/src/app/(default)/load-test/IndexList.tsx new file mode 100644 index 000000000..bd9644467 --- /dev/null +++ b/packages/app/src/app/(default)/load-test/IndexList.tsx @@ -0,0 +1,138 @@ +"use client"; + +import Table from "@codegouvfr/react-dsfr/Table"; +import { CompanyWorkforceRange } from "@common/core-domain/domain/valueObjects/declaration/CompanyWorkforceRange"; +import { type DeclarationDTO } from "@common/core-domain/dtos/DeclarationDTO"; +import { type DeclarationOpmcDTO } from "@common/core-domain/dtos/DeclarationOpmcDTO"; +import { formatIsoToFr } from "@common/utils/date"; +import { Heading, Link } from "@design-system"; +import { isBefore, sub } from "date-fns"; +import { capitalize, upperCase } from "lodash"; + +import { buildHelpersObjectifsMesures } from "../index-egapro/objectifs-mesures/[siren]/[year]/ObjectifsMesuresForm"; + +//Note: For 2022, first year of OPMC, we consider that the duration to be frozen is 2 years, but for next years, it will be 1 year like isFrozenDeclaration. +const OPMC_FROZEN_DURATION = { years: 2 }; + +const isFrozenDeclarationForOPMC = (declaration: DeclarationOpmcDTO) => + declaration?.["declaration-existante"]?.date + ? isBefore(new Date(declaration?.["declaration-existante"]?.date), sub(new Date(), OPMC_FROZEN_DURATION)) + : false; + +enum declarationOpmcStatus { + ALREADY_FILLED = "Déjà renseignés", + COMPLETED = "Renseignés", + INDEX_OVER_85 = "Index supérieur à 85", + NOT_APPLICABLE = "Non applicable", + NOT_MODIFIABLE = "Déclaration non modifiable", + NOT_MODIFIABLE_CORRECT = "Déclaration non modifiable sur données correctes", + NOT_MODIFIABLE_INCORRECT = "Déclaration non modifiable sur données incorrectes", + TO_COMPLETE = "À renseigner", + YEAR_NOT_APPLICABLE = "Année non applicable", +} + +const getDeclarationOpmcStatus = (declaration?: DeclarationOpmcDTO) => { + if (!declaration) return declarationOpmcStatus.NOT_APPLICABLE; + const { after2021, index, initialValuesObjectifsMesures, objectifsMesuresSchema } = + buildHelpersObjectifsMesures(declaration); + + if (!declaration["resultat-global"] || index === undefined) return declarationOpmcStatus.NOT_APPLICABLE; + if (!after2021) return declarationOpmcStatus.YEAR_NOT_APPLICABLE; + if (index >= 85) return declarationOpmcStatus.INDEX_OVER_85; + + try { + objectifsMesuresSchema.parse(initialValuesObjectifsMesures); + if (isFrozenDeclarationForOPMC(declaration)) return declarationOpmcStatus.NOT_MODIFIABLE_CORRECT; + } catch (e) { + if (isFrozenDeclarationForOPMC(declaration)) return declarationOpmcStatus.NOT_MODIFIABLE_INCORRECT; + return declarationOpmcStatus.TO_COMPLETE; + } + return declarationOpmcStatus.COMPLETED; +}; + +const formatDeclarationOpmcStatus = (status: declarationOpmcStatus, siren: string, year: number) => { + const withLink = (text: string) => ( + + {text} + + ); + + switch (status) { + case declarationOpmcStatus.COMPLETED: + return withLink(declarationOpmcStatus.COMPLETED); + case declarationOpmcStatus.INDEX_OVER_85: + return declarationOpmcStatus.INDEX_OVER_85; + case declarationOpmcStatus.NOT_APPLICABLE: + return declarationOpmcStatus.NOT_APPLICABLE; + case declarationOpmcStatus.NOT_MODIFIABLE_CORRECT: + return withLink(declarationOpmcStatus.ALREADY_FILLED); + case declarationOpmcStatus.NOT_MODIFIABLE_INCORRECT: + return withLink(declarationOpmcStatus.NOT_MODIFIABLE); + case declarationOpmcStatus.TO_COMPLETE: + return withLink(declarationOpmcStatus.TO_COMPLETE); + case declarationOpmcStatus.YEAR_NOT_APPLICABLE: + return declarationOpmcStatus.YEAR_NOT_APPLICABLE; + default: + return declarationOpmcStatus.NOT_APPLICABLE; + } +}; + +const formatTableData = (declarations: DeclarationDTO[], declarationOpmcList: DeclarationOpmcDTO[]) => + declarations.map(declaration => { + const rowYear = declaration.commencer?.annéeIndicateurs; + const rowSiren = declaration.commencer?.siren; + return [ + + {declaration.commencer?.siren} + , + rowYear, + declaration.entreprise?.type === "ues" + ? upperCase(declaration.entreprise?.type) + : capitalize(declaration.entreprise?.type), + declaration.entreprise?.tranche ? CompanyWorkforceRange.Label[declaration.entreprise.tranche] : undefined, + formatIsoToFr(declaration["declaration-existante"].date || ""), + declaration["resultat-global"]?.index || NC, + formatDeclarationOpmcStatus( + getDeclarationOpmcStatus( + declarationOpmcList.find(declarationOpmc => declarationOpmc.commencer?.annéeIndicateurs === rowYear), + ), + rowSiren || "", + rowYear || 0, + ), + + Télécharger + , + ]; + }); + +export const IndexList = ({ + declarations, + declarationOpmcList, +}: { + declarationOpmcList: DeclarationOpmcDTO[]; + declarations: DeclarationDTO[]; +}) => { + const headers = [ + "SIREN", + "ANNÉE INDICATEUR", + "STRUCTURE", + "TRANCHE D'EFFECTIF", + "DATE DE DÉCLARATION", + "INDEX", + "OBJECTIFS ET MESURES", + "RÉCAPITULATIF", + ]; + + return ( +
+ + {(declarations.length > 0 && ( + + )) ||

Aucune déclaration transmise.

} + + ); +}; diff --git a/packages/app/src/app/(default)/load-test/RepeqList.tsx b/packages/app/src/app/(default)/load-test/RepeqList.tsx new file mode 100644 index 000000000..7eb1d6e31 --- /dev/null +++ b/packages/app/src/app/(default)/load-test/RepeqList.tsx @@ -0,0 +1,62 @@ +import Table from "@codegouvfr/react-dsfr/Table"; +import { type RepresentationEquilibreeDTO } from "@common/core-domain/dtos/RepresentationEquilibreeDTO"; +import { formatIsoToFr } from "@common/utils/date"; +import { Heading, Link } from "@design-system"; + +const getPercent = ( + filterReasonKey: string, + percentKey: string, + representationEquilibree: RepresentationEquilibreeDTO, +) => + (!(filterReasonKey in representationEquilibree) && + representationEquilibree[percentKey as keyof RepresentationEquilibreeDTO]?.toString()) || + "NC"; + +const formatTableData = (representationEquilibrees: RepresentationEquilibreeDTO[]) => + representationEquilibrees.map(representationEquilibree => [ + + {representationEquilibree.siren} + , + representationEquilibree.year, + formatIsoToFr(representationEquilibree.declaredAt), + getPercent("notComputableReasonExecutives", "executiveWomenPercent", representationEquilibree), + getPercent("notComputableReasonExecutives", "executiveMenPercent", representationEquilibree), + getPercent("notComputableReasonMembers", "memberWomenPercent", representationEquilibree), + getPercent("notComputableReasonMembers", "memberMenPercent", representationEquilibree), + + Télécharger + , + ]); + +export const RepeqList = ({ + representationEquilibrees, +}: { + representationEquilibrees: RepresentationEquilibreeDTO[]; +}) => { + const headers = [ + "SIREN", + "ANNÉE ÉCARTS", + "DATE DE DÉCLARATION", + "% FEMMES CADRES", + "% HOMMES CADRES", + "% FEMMES MEMBRES", + "% HOMMES MEMBRES", + "RÉCAPITULATIF", + ]; + + return ( +
+ + {(representationEquilibrees.length > 0 && ( +
+ )) ||

Aucune déclaration transmise.

} + + ); +}; diff --git a/packages/app/src/app/(default)/load-test/page.tsx b/packages/app/src/app/(default)/load-test/page.tsx new file mode 100644 index 000000000..d18d755fc --- /dev/null +++ b/packages/app/src/app/(default)/load-test/page.tsx @@ -0,0 +1,80 @@ +import Alert from "@codegouvfr/react-dsfr/Alert"; +import { type DeclarationDTO } from "@common/core-domain/dtos/DeclarationDTO"; +import { type DeclarationOpmcDTO } from "@common/core-domain/dtos/DeclarationOpmcDTO"; +import { type RepresentationEquilibreeDTO } from "@common/core-domain/dtos/RepresentationEquilibreeDTO"; +import { type NextServerPageProps } from "@common/utils/next"; +import { Box, Heading } from "@design-system"; +import { MessageProvider } from "@design-system/client"; + +import { + getAllDeclarationOpmcSirenAndYear, + getAllDeclarationsBySiren, + getAllRepresentationEquilibreeBySiren, +} from "../mon-espace/actions"; +import { IndexList } from "./IndexList"; +import { RepeqList } from "./RepeqList"; + +const InfoText = () => ( + <> +

+ Dans ce menu, vous avez accès à la liste des déclarations de l’index de l’égalité professionnelle et, si vous êtes + assujetti, de la représentation équilibrée qui ont été transmises à l’administration, en sélectionnant au + préalable dans la liste déroulante le numéro Siren de l'entreprise (ou de l’entreprise ayant déclaré l'index pour + le compte de l’unité économique et sociale) concernée si vous gérez plusieurs entreprises. +

+
+

+ Vous pouvez ainsi télécharger le récapitulatif de la déclaration à la colonne « RÉCAPITULATIF », et en + cliquant sur le Siren, vous accédez à la déclaration transmise. A la colonne « OBJECTIFS ET MESURES », vous + avez accès à la déclaration des mesures de correction lorsque l’index est inférieur à 75 points et des objectifs + de progression lorsque l’index est inférieur à 85 points. +

+ +); + +const LoadTestPage = async ({ searchParams }: NextServerPageProps) => { + try { + if (!searchParams || !searchParams.siren || !searchParams.key) throw new Error("Missing search params"); + if (searchParams.key !== "egapro-load-test") throw new Error("Invalid key"); + } catch (e) { + throw new Error("Load test error"); + } + + const selectedSiren = searchParams.siren; + + let declarations: DeclarationDTO[] = []; + let repEq: RepresentationEquilibreeDTO[] = []; + const declarationOpmcList: DeclarationOpmcDTO[] = []; + + if (selectedSiren) { + declarations = await getAllDeclarationsBySiren(selectedSiren); + repEq = await getAllRepresentationEquilibreeBySiren(selectedSiren); + + for (const declaration of declarations) { + if (declaration.commencer?.annéeIndicateurs) { + const result = await getAllDeclarationOpmcSirenAndYear( + declaration.commencer?.siren || "", + declaration.commencer?.annéeIndicateurs || 0, + ); + if (result) { + declarationOpmcList.push(result); + } + } + } + } + + return ( + + + } /> + + + + + + + + ); +}; + +export default LoadTestPage; diff --git a/packages/app/src/app/api/public/load-test/route.ts b/packages/app/src/app/api/public/load-test/route.ts new file mode 100644 index 000000000..9e4a9731a --- /dev/null +++ b/packages/app/src/app/api/public/load-test/route.ts @@ -0,0 +1,165 @@ +import { entrepriseService } from "@api/core-domain/infra/services"; +import { declarationRepo } from "@api/core-domain/repo"; +import { SaveDeclaration } from "@api/core-domain/useCases/SaveDeclaration"; +import { DeclarationSpecificationError } from "@common/core-domain/domain/specification/DeclarationSpecification"; +import { type CreateDeclarationDTO } from "@common/core-domain/dtos/DeclarationDTO"; +import { ValidationError } from "@common/shared-domain"; +import { type NextRouteHandler } from "@common/utils/next"; +import { NextResponse } from "next/server"; + +// Note: [revalidatePath bug](https://github.com/vercel/next.js/issues/49387). Try to reactivate it when it will be fixed in Next (it seems to be fixed in Next 14). +// export const revalidate = 86400; // 24h +export const dynamic = "force-dynamic"; +// export const revalidate = 86_400; // 24h + +export const POST: NextRouteHandler = async (req: Request) => { + const body = await req.json(); + + if (!body) return NextResponse.json({ ok: false, error: "No body" }); + if (body.key !== "egapro-load-test") return NextResponse.json({ ok: false, error: "unauthorized" }); + + const declaration = { + "declaration-existante": { + status: "creation", + }, + "periode-reference": { + périodeSuffisante: "oui", + finPériodeRéférence: "2021-12-31", + effectifTotal: 477, + }, + entreprise: { + type: "entreprise", + tranche: "251:999", + entrepriseDéclarante: { + adresse: "96 RUE DE LEVIS 75017 PARIS 17", + codeNaf: "62.01Z", + codePostal: "75017", + raisonSociale: "PERRAULT", + siren: "532386398", + commune: "PARIS 17", + département: "75", + région: "11", + }, + }, + augmentations: { + estCalculable: "oui", + populationFavorable: "hommes", + résultat: 2.1, + note: 10, + catégories: { + ouv: 0, + emp: "", + tam: 2.4, + ic: "", + }, + }, + promotions: { + estCalculable: "oui", + populationFavorable: "hommes", + résultat: 1.4, + note: 15, + catégories: { + ouv: 5, + emp: "", + tam: 0.9, + ic: "", + }, + }, + "remunerations-resultat": { + note: 40, + résultat: 0, + }, + remunerations: { + estCalculable: "oui", + mode: "csp", + }, + "remunerations-csp": { + catégories: [ + { + nom: "ouv", + tranches: { + ":29": "", + "30:39": "", + "40:49": -1.118, + "50:": -2.906, + }, + }, + { + nom: "emp", + tranches: { + ":29": "", + "30:39": "", + "40:49": "", + "50:": "", + }, + }, + { + nom: "tam", + tranches: { + ":29": -0.941, + "30:39": -0.854, + "40:49": 0.583, + "50:": -1.369, + }, + }, + { + nom: "ic", + tranches: { + ":29": "", + "30:39": "", + "40:49": "", + "50:": "", + }, + }, + ], + }, + "conges-maternite": { + estCalculable: "non", + motifNonCalculabilité: "absaugpdtcm", + }, + "hautes-remunerations": { + populationFavorable: "femmes", + résultat: 3, + note: 5, + }, + commencer: { + annéeIndicateurs: 2021, + siren: "532386398", + }, + declarant: { + accordRgpd: true, + email: "jonathan.perrault@gmail.com", + nom: "Perrault", + prénom: "Jonathan", + téléphone: "0749968164", + }, + "resultat-global": { + index: 82, + points: 70, + pointsCalculables: 85, + }, + publication: { + choixSiteWeb: "non", + date: "2024-01-31", + modalités: "gezg", + planRelance: "oui", + }, + } as CreateDeclarationDTO; + + try { + const useCase = new SaveDeclaration(declarationRepo, entrepriseService); + await useCase.execute({ declaration }); + return NextResponse.json({ ok: true }); + } catch (error: unknown) { + if (error instanceof DeclarationSpecificationError || error instanceof ValidationError) { + return NextResponse.json({ + ok: false, + error: error.message ?? error.previousError, + }); + } + return NextResponse.json({ + ok: false, + error: "Une erreur est survenue, veuillez réessayer.", + }); + } +};