From 4ab79b9b43ae40dd7517987ae732027e241fde86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Huchet?= Date: Tue, 4 Oct 2022 15:27:14 +0200 Subject: [PATCH] feat(cron): traitement errone (#258) * feat(cron): traitement errone * chore: up * chore: up * chore: up * fix: up * fix: up * fix: add cron --- .../templates/reportingTraitementErrone.yaml | 28 +++ .../cron/reportingTraitementErrone.spec.ts | 233 ++++++++++++++++++ src/cron/launch.ts | 2 + src/cron/reportingTraitementErrone.ts | 145 +++++++++++ .../demarchesSimplifiees/buildRequest.ts | 21 +- 5 files changed, 427 insertions(+), 2 deletions(-) create mode 100644 .kube-workflow/prod/templates/reportingTraitementErrone.yaml create mode 100644 src/__tests__/cron/reportingTraitementErrone.spec.ts create mode 100644 src/cron/reportingTraitementErrone.ts diff --git a/.kube-workflow/prod/templates/reportingTraitementErrone.yaml b/.kube-workflow/prod/templates/reportingTraitementErrone.yaml new file mode 100644 index 00000000..440ef208 --- /dev/null +++ b/.kube-workflow/prod/templates/reportingTraitementErrone.yaml @@ -0,0 +1,28 @@ +apiVersion: batch/v1beta1 +kind: CronJob +metadata: + namespace: mon-psy-sante + name: reporting-traitement-errone + labels: + app: mon-psy +spec: + schedule: "0 11 * * 2" + jobTemplate: + spec: + template: + spec: + containers: + - name: mon-psy + image: ghcr.io/socialgouv/mon-psy-sante/app:{{ .Values.global.imageTag }} + envFrom: + - secretRef: + name: app-sealed-secret + - secretRef: + name: pg-user + command: + - yarn + - run + - cron:launch + - reportingTraitementErrone + restartPolicy: Never + concurrencyPolicy: Forbid diff --git a/src/__tests__/cron/reportingTraitementErrone.spec.ts b/src/__tests__/cron/reportingTraitementErrone.spec.ts new file mode 100644 index 00000000..302bcdfd --- /dev/null +++ b/src/__tests__/cron/reportingTraitementErrone.spec.ts @@ -0,0 +1,233 @@ +import { + cpamOnly, + notificationSelectionNotChecked, + withoutInstructeurFB, +} from "../../cron/reportingTraitementErrone"; +import { request } from "../../services/demarchesSimplifiees/request"; +import { DSPsychologist } from "../../types/psychologist"; + +const INSTRUCTEUR_FB = "SW5zdHJ1Y3RldXItNjQzNjI="; +const DOSSIER_ELIGIBLE = "Q2hhbXAtMTg0NDM5NQ=="; +const NOTIFICATION_SELECTION = "Q2hhbXAtMjMyMzA2Mg=="; + +jest.mock("../../services/demarchesSimplifiees/request"); + +describe("Cron import from DS", () => { + function mockDSCall(psychologistsInDS: Partial[]) { + const mockRequest = request; + mockRequest.mockImplementationOnce(() => + Promise.resolve({ + demarche: { dossiers: { nodes: psychologistsInDS, pageInfo: {} } }, + }) + ); + } + + describe("`cpamOnly()`: dossiers en construction with only CPAM instructeurs", () => { + it("should filter out dossiers with no instructeurs", async () => { + mockDSCall([ + { + id: "1", + instructeurs: [], + }, + ]); + expect(await cpamOnly()).toHaveLength(0); + }); + + it("should filter out dossiers with internal instructeurs and no CPAM instructeurs", async () => { + mockDSCall([ + { + id: "1", + instructeurs: [ + { id: "SW5zdHJ1Y3RldXItNjExNTM=", email: "x@example.org" }, + { id: "SW5zdHJ1Y3RldXItNDk3NDI=", email: "y@example.org" }, + ], + }, + ]); + expect(await cpamOnly()).toHaveLength(0); + }); + + it("should filter out dossiers with internal instructeurs and CPAM instructeurs", async () => { + mockDSCall([ + { + id: "1", + instructeurs: [ + { id: "SW5zdHJ1Y3RldXItNjExNTM=", email: "x@example.org" }, + { id: "xxxxxxx", email: "cpam@example.org" }, + ], + }, + ]); + expect(await cpamOnly()).toHaveLength(0); + }); + + it("should include dossiers with no internal instructeurs and one CPAM instructeurs", async () => { + mockDSCall([ + { + id: "1", + instructeurs: [{ id: "xxxxxxx", email: "cpam@example.org" }], + }, + ]); + expect(await cpamOnly()).toHaveLength(1); + }); + it("should include dossiers with no internal instructeurs and some CPAM instructeurs", async () => { + mockDSCall([ + { + id: "1", + instructeurs: [ + { id: "xxxxxxx", email: "cpam@example.org" }, + { id: "yyyyyyy", email: "cpam2@example.org" }, + ], + }, + ]); + expect(await cpamOnly()).toHaveLength(1); + }); + }); + + describe("`notificationSelectionNotChecked()`: dossiers with no notification selection", () => { + it("should filter out dossiers with notification selection checked", async () => { + mockDSCall([ + { + id: "1", + annotations: [ + { + id: NOTIFICATION_SELECTION, + label: "Notification Sélection", + stringValue: "oui", + }, + ], + }, + ]); + expect(await notificationSelectionNotChecked()).toHaveLength(0); + }); + it("should filter out dossiers where dossier elligible is not OUI or NON", async () => { + mockDSCall([ + { id: "0", annotations: [] }, + { + id: "1", + annotations: [ + { + id: DOSSIER_ELIGIBLE, + label: "Dossier elligible", + stringValue: "peut-être", + }, + ], + }, + { + id: "2", + annotations: [ + { + id: NOTIFICATION_SELECTION, + label: "Notification Sélection", + stringValue: "", + }, + { + id: DOSSIER_ELIGIBLE, + label: "Dossier elligible", + stringValue: "peut-être", + }, + ], + }, + ]); + expect(await notificationSelectionNotChecked()).toHaveLength(0); + }); + it("should include dossiers with notification selection not checked and dossier elligible OUI or NON", async () => { + mockDSCall([ + { + id: "1", + annotations: [ + { + id: DOSSIER_ELIGIBLE, + label: "Dossier elligible", + stringValue: "OUI", + }, + { + id: NOTIFICATION_SELECTION, + label: "Notification Sélection", + stringValue: "", + }, + ], + }, + { + id: "2", + annotations: [ + { + id: NOTIFICATION_SELECTION, + label: "Notification Sélection", + stringValue: "", + }, + { + id: DOSSIER_ELIGIBLE, + label: "Dossier elligible", + stringValue: "NON", + }, + ], + }, + ]); + expect(await notificationSelectionNotChecked()).toHaveLength(2); + }); + }); + + describe("`withoutInstructeurFB()`: dossiers en instruction elligible without FB as an inscructor", () => { + it("should filter out dossiers where dossier elligible is not OUI or NON", async () => { + mockDSCall([ + { id: "0", annotations: [] }, + { + id: "1", + annotations: [ + { + id: DOSSIER_ELIGIBLE, + label: "Dossier elligible", + stringValue: "peut-être", + }, + ], + }, + ]); + expect(await withoutInstructeurFB()).toHaveLength(0); + }); + it("should filter out dossiers where dossier elligible is OUI or NON and FB is an instructeur", async () => { + mockDSCall([ + { + id: "1", + annotations: [ + { + id: DOSSIER_ELIGIBLE, + label: "Dossier elligible", + stringValue: "OUI", + }, + ], + instructeurs: [{ id: INSTRUCTEUR_FB, email: "x@example.org" }], + }, + { + id: "2", + annotations: [ + { + id: DOSSIER_ELIGIBLE, + label: "Dossier elligible", + stringValue: "NON", + }, + ], + instructeurs: [ + { id: INSTRUCTEUR_FB, email: "x@example.org" }, + { id: "yyyyyyy", email: "y@example.org" }, + ], + }, + ]); + expect(await withoutInstructeurFB()).toHaveLength(0); + }); + it("should include dossiers where dossier elligible is OUI or NON and FB is not an instructeur", async () => { + mockDSCall([ + { + id: "1", + annotations: [ + { + id: DOSSIER_ELIGIBLE, + label: "Dossier elligible", + stringValue: "OUI", + }, + ], + instructeurs: [{ id: "yyyyyyy", email: "y@example.org" }], + }, + ]); + expect(await withoutInstructeurFB()).toHaveLength(1); + }); + }); +}); diff --git a/src/cron/launch.ts b/src/cron/launch.ts index a59d0b9d..91d4255e 100644 --- a/src/cron/launch.ts +++ b/src/cron/launch.ts @@ -5,6 +5,7 @@ import { reportingDossierEligible } from "./reportingDossierEligible"; import { reportingDossierRefuse } from "./reportingDossierRefuse"; import { reportingExpertWeekly } from "./reportingExpertWeekly"; import { reportingStatsByDepartment } from "./reportingStatsByDepartment"; +import { reportingTraitementErrone } from "./reportingTraitementErrone"; const runJob = async (job): Promise => { const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN; @@ -33,6 +34,7 @@ const cronJobs = { reportingExpertWeekly, reportingDossierEligible, reportingDossierRefuse, + reportingTraitementErrone, }; const jobName = process.argv[2]; diff --git a/src/cron/reportingTraitementErrone.ts b/src/cron/reportingTraitementErrone.ts new file mode 100644 index 00000000..93dcb33f --- /dev/null +++ b/src/cron/reportingTraitementErrone.ts @@ -0,0 +1,145 @@ +// https://www.notion.so/fabnummas/Export-mardi-14h-traitement-erron-des-dossiers-96eb6d8b0f734556b7176758f68372fb +import { + requestDossiersAllState, + requestDossiersEnConstruction, + requestDossiersEnInstruction, +} from "../services/demarchesSimplifiees/buildRequest"; +import { getAllPsychologistList } from "../services/demarchesSimplifiees/import"; +import { sendEmailWithAttachments } from "./cronUtils"; + +const INSTRUCTEURS = { + "SW5zdHJ1Y3RldXItNjMyMjc=": "BS", + "SW5zdHJ1Y3RldXItNjMyMzU=": "ACB", + "SW5zdHJ1Y3RldXItNjMyMjk=": "ADS", + "SW5zdHJ1Y3RldXItNjMyMzE=": "ES", + "SW5zdHJ1Y3RldXItNjMyMzA=": "ADT", + "SW5zdHJ1Y3RldXItNjMyMjg=": "SCO", + "SW5zdHJ1Y3RldXItNjExNTM=": "Beta-Gouv-AG", + "SW5zdHJ1Y3RldXItNDk3NDI=": "Beta-Gouv-LG", +}; +const INSTRUCTEUR_FB = "SW5zdHJ1Y3RldXItNjQzNjI="; +const DOSSIER_ELIGIBLE = "Q2hhbXAtMTg0NDM5NQ=="; +const NOTIFICATION_SELECTION = "Q2hhbXAtMjMyMzA2Mg=="; + +// Original request: "des dossiers en construction ouverts par un instructeur CPAM exclusivement" +export async function cpamOnly() { + const result = await getAllPsychologistList( + requestDossiersEnConstruction + ).catch((e) => { + console.log(e); + process.exit(-1); + }); + + return result.psychologists + .filter((psychologist) => { + const instructeurs = psychologist.instructeurs.map((i) => i.id); + const instructeursWithCpamOnly = instructeurs.filter( + (instructeur) => !INSTRUCTEURS[instructeur] + ); + return ( + instructeursWithCpamOnly.length === instructeurs.length && + instructeursWithCpamOnly.length > 0 + ); + }) + .map((psychologist) => { + return psychologist.number; + }); +} + +// Original request: "des dossiers tout statut confondu cochés éligibles (”OUI”) et non éligibles (”NON”) et dont la “notification de la sélection” n’a pas été cochée" +export async function notificationSelectionNotChecked() { + const result = await getAllPsychologistList(requestDossiersAllState).catch( + (e) => { + console.log(e); + process.exit(-1); + } + ); + + return result.psychologists + .filter((psy) => { + const dossierEligibleValue = psy.annotations.find( + (a) => a.id === DOSSIER_ELIGIBLE + )?.stringValue; + const notificationSelectionValue = psy.annotations.find( + (a) => a.id === NOTIFICATION_SELECTION + )?.stringValue; + return ( + ["OUI", "NON"].includes( + (dossierEligibleValue || "").toUpperCase().trim() + ) && + // "Notification Sélection" empty + !(notificationSelectionValue || "").toUpperCase().trim() + ); + }) + .map((psychologist) => { + return psychologist.number; + }); +} + +// Original request: "des dossiers en instruction coché éligibles “Oui” ou “Non” sans l’instructeur INSTRUCTEUR_FB (dans les “instructeurs”)" +export async function withoutInstructeurFB() { + const result = await getAllPsychologistList( + requestDossiersEnInstruction + ).catch((e) => { + console.log(e); + process.exit(-1); + }); + + return result.psychologists + .filter((psy) => { + const dossierEligibleValue = psy.annotations.find( + (a) => a.id === DOSSIER_ELIGIBLE + )?.stringValue; + return ( + ["OUI", "NON"].includes( + (dossierEligibleValue || "").toUpperCase().trim() + ) && + psy.instructeurs.filter((i) => i.id === INSTRUCTEUR_FB).length === 0 + ); + }) + .map((psychologist) => { + return psychologist.number; + }); +} + +export async function reportingTraitementErrone() { + console.log("Récupération des traitements erronés"); + + console.log("Récupération CPAM"); + const cpamOnlyList = await cpamOnly(); + console.log(cpamOnlyList.join("\n")); + + console.log("Récupération sélection non cochée"); + const notificationSelectionNotCheckedList = + await notificationSelectionNotChecked(); + console.log(notificationSelectionNotCheckedList.join("\n")); + + console.log("Récupération sans instructeur FB"); + const withoutInstructeurFBList = await withoutInstructeurFB(); + console.log(withoutInstructeurFBList.join("\n")); + + return sendEmailWithAttachments({ + subject: "Fichiers de suivi des traitements erronés", + textSlices: [ + "Bonjour,", + "Ci-joint le fichier de suivi des traitements erronés. Les trois fichiers concernent les trois demandes suivantes :", + "1. des dossiers en construction ouverts par un instructeur CPAM exclusivement", + "2. des dossiers tout statut confondu cochés éligibles (”OUI”) et non éligibles (”NON”) et dont la “notification de la sélection” n’a pas été cochée", + "3. des dossiers en instruction coché éligibles “Oui” ou “Non” sans l’instructeur INSTRUCTEUR_FB (dans les “instructeurs”)", + ], + attachments: [ + { + filename: `1-construction-cpam-only.csv`, + content: Buffer.from(cpamOnlyList.join("\n")), + }, + { + filename: `2-tous-etat-notification-selection-vide.csv`, + content: Buffer.from(notificationSelectionNotCheckedList.join("\n")), + }, + { + filename: `3-instruction-sans-instructeur-fb.csv`, + content: Buffer.from(withoutInstructeurFBList.join("\n")), + }, + ], + }); +} diff --git a/src/services/demarchesSimplifiees/buildRequest.ts b/src/services/demarchesSimplifiees/buildRequest.ts index 134c7f44..ae10d179 100644 --- a/src/services/demarchesSimplifiees/buildRequest.ts +++ b/src/services/demarchesSimplifiees/buildRequest.ts @@ -219,16 +219,33 @@ export const requestDossiersEnInstruction = async ( return requestDossiersByState(DossierState.enInstruction, afterCursor); }; +export const requestDossiersAccepte = async ( + afterCursor: string | undefined +): Promise => { + return requestDossiersByState(DossierState.accepte, afterCursor); +}; + +export const requestDossiersAllState = async ( + afterCursor: string | undefined +): Promise => { + return requestDossiersByState(null, afterCursor); +}; + export const requestDossiersByState = async ( - state: DossierState, + state: DossierState | null, afterCursor: string | undefined, extraInfos?: string | undefined ): Promise => { const paginationCondition = getWhereConditionAfterCursor(afterCursor); + const stateCondition = state ? `state: ${state}` : ""; const query = gql` { demarche (number: ${config.demarchesSimplifiees.id}) { - dossiers (state: ${state}${paginationCondition}) { + dossiers ${ + stateCondition || paginationCondition + ? "(" + stateCondition + paginationCondition + ")" + : "" + } { pageInfo { hasNextPage endCursor