From 075d9e8f95e672f1d60baa96a51e916585125ef7 Mon Sep 17 00:00:00 2001
From: Anis Safine Laget <anis.safine@beta.gouv.fr>
Date: Thu, 13 Mar 2025 17:33:03 +0100
Subject: [PATCH 1/4] =?UTF-8?q?d=C3=A9but=20script=20de=20fix=20des=20prol?=
 =?UTF-8?q?ongations=20incompl=C3=A9tes?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/api/package.json                     |   2 +-
 .../api/src/api/rest/demarches.queries.ts     |   2 +-
 .../api/src/business/check-prolongations.ts   | 284 ++++++++++++++++++
 .../api/src/scripts/check-prolongations.ts    |  14 +
 4 files changed, 300 insertions(+), 2 deletions(-)
 create mode 100644 packages/api/src/business/check-prolongations.ts
 create mode 100644 packages/api/src/scripts/check-prolongations.ts

diff --git a/packages/api/package.json b/packages/api/package.json
index e38eef6ed..75cb20513 100644
--- a/packages/api/package.json
+++ b/packages/api/package.json
@@ -7,7 +7,7 @@
   "type": "module",
   "scripts": {
     "build": "tsc --incremental",
-    "check-perimetres": "node --enable-source-maps --loader ts-node/esm/transpile-only ./src/scripts/check-perimetres.ts",
+    "check-prolongations": "node --enable-source-maps --loader ts-node/esm/transpile-only ./src/scripts/check-prolongations.ts",
     "daily": "node --enable-source-maps --loader ts-node/esm/transpile-only ./src/scripts/daily.ts",
     "monthly": "node --enable-source-maps --loader ts-node/esm/transpile-only ./src/scripts/monthly.ts",
     "db:dump": "rm -rf ./backups/* && pg_dump --host=localhost --username=postgres --clean --if-exists --format=d --no-owner --no-privileges --dbname=camino --file=./backups/",
diff --git a/packages/api/src/api/rest/demarches.queries.ts b/packages/api/src/api/rest/demarches.queries.ts
index 9623f992d..18f583930 100644
--- a/packages/api/src/api/rest/demarches.queries.ts
+++ b/packages/api/src/api/rest/demarches.queries.ts
@@ -435,7 +435,7 @@ FROM (
 ) tmp
 `
 const getDemarchesValidator = z.object({ id: demarcheIdValidator })
-type GetDemarche = z.infer<typeof getDemarchesValidator>
+export type GetDemarche = z.infer<typeof getDemarchesValidator>
 export const getDemarches = (pool: Pool): Effect.Effect<GetDemarche[], CaminoError<EffectDbQueryAndValidateErrors>> => effectDbQueryAndValidate(getDemarchesDb, {}, pool, getDemarchesValidator)
 
 const getDemarchesDb = sql<Redefine<IGetDemarchesDbQuery, {}, GetDemarche>>`
diff --git a/packages/api/src/business/check-prolongations.ts b/packages/api/src/business/check-prolongations.ts
new file mode 100644
index 000000000..9e80e263a
--- /dev/null
+++ b/packages/api/src/business/check-prolongations.ts
@@ -0,0 +1,284 @@
+import { Pool } from 'pg'
+import { getDemarcheByIdOrSlug, GetDemarcheByIdOrSlugErrors, getDemarches, getEtapesByDemarcheId } from '../api/rest/demarches.queries'
+import { Effect, pipe } from 'effect'
+import { CaminoError } from 'camino-common/src/zod-tools'
+import { DemarcheId } from 'camino-common/src/demarche'
+import { isEtapeComplete } from 'camino-common/src/permissions/titres-etapes'
+import { userSuper } from '../database/user-super'
+import { CaminoDate, firstEtapeDateValidator } from 'camino-common/src/date'
+import { titreEtapeGet, titreEtapeUpsert } from '../database/queries/titres-etapes'
+import { isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty, isNullOrUndefined, isNullOrUndefinedOrEmpty, memoize, toSorted } from 'camino-common/src/typescript-tools'
+import { iTitreEtapeToFlattenEtape, TitreEtapeToFlattenEtapeErrors } from '../api/_format/titres-etapes'
+import { ITitreEtape } from '../types'
+import {
+  getDocumentsByEtapeId,
+  getEntrepriseDocumentIdsByEtapeId,
+  getEtapeAvisLargeObjectIdsByEtapeId,
+  GetEtapeAvisLargeObjectIdsByEtapeIdErrors,
+  GetEtapeDocumentLargeObjectIdsByEtapeIdErrors,
+} from '../database/queries/titres-etapes.queries'
+import { getAdministrationsLocales } from 'camino-common/src/administrations'
+import { ZodUnparseable } from '../tools/fp-tools'
+import { TitreId, TitreSlug } from 'camino-common/src/validators/titres'
+import { EtapeId } from 'camino-common/src/etape'
+import { isDemarcheTypeProlongations } from 'camino-common/src/static/demarchesTypes'
+import { SUBSTANCES_FISCALES_IDS } from 'camino-common/src/static/substancesFiscales'
+import { entrepriseIdValidator } from 'camino-common/src/entreprise'
+import { km2Validator } from 'camino-common/src/number'
+import { FeatureMultiPolygon } from 'camino-common/src/perimetre'
+
+type CheckedProlongation = {
+  titre_slug: TitreSlug | null
+  demarche_id: DemarcheId
+  etape_id: EtapeId | null
+  etape_date: CaminoDate | null
+  titre_id: TitreId | null
+  errors: string[]
+}
+
+const demarchePasProlongationError = 'pas une prolongation' as const
+const demarcheSansEtapesError = "La démarche ne contient pas d'étapes" as const
+const titreEtapeGetError = 'Le fetch de la firstEtape a échoué' as const
+type CheckOnePrologationErrors =
+  | GetDemarcheByIdOrSlugErrors
+  | TitreEtapeToFlattenEtapeErrors
+  | GetEtapeDocumentLargeObjectIdsByEtapeIdErrors
+  | ZodUnparseable
+  | GetEtapeAvisLargeObjectIdsByEtapeIdErrors
+  | typeof titreEtapeGetError
+  | typeof demarcheSansEtapesError
+  | typeof demarchePasProlongationError
+const checkOneProlongation = (pool: Pool, demarcheId: DemarcheId): Effect.Effect<CheckedProlongation, CaminoError<CheckOnePrologationErrors>> => {
+  return Effect.Do.pipe(
+    Effect.bind('demarche', () => getDemarcheByIdOrSlug(pool, demarcheId)),
+    Effect.filterOrFail(
+      ({ demarche }) => isDemarcheTypeProlongations(demarche.demarche_type_id),
+      () => ({ message: demarchePasProlongationError })
+    ),
+    Effect.bind('etapes', () => getEtapesByDemarcheId(pool, demarcheId)),
+    Effect.filterOrFail(
+      ({ etapes }) => isNotNullNorUndefinedNorEmpty(etapes),
+      () => ({ message: demarcheSansEtapesError, extra: {} })
+    ),
+    Effect.bind('firstEtape', ({ etapes }) =>
+      pipe(
+        Effect.tryPromise({
+          try: () => titreEtapeGet(toSorted(etapes, (a, b) => a.ordre - b.ordre)[0].id, { fields: { id: {} }, fetchHeritage: true }, userSuper),
+          catch: error => ({ message: titreEtapeGetError, extra: error }),
+        }),
+        Effect.filterOrFail(
+          (firstEtape): firstEtape is ITitreEtape => isNotNullNorUndefined(firstEtape),
+          () => ({ message: titreEtapeGetError, extra: 'La first étape est null ou undefined' })
+        )
+      )
+    ),
+    Effect.let('titreProps', ({ demarche, firstEtape }) => ({
+      titreTypeId: memoize(() => Promise.resolve(demarche.titre_type_id)),
+      administrationsLocales: memoize(() =>
+        Promise.resolve(getAdministrationsLocales(isNotNullNorUndefined(firstEtape.communes) ? firstEtape.communes.map(({ id }) => id) : [], firstEtape.secteursMaritime))
+      ),
+      entreprisesTitulairesOuAmodiataires: memoize(() => Promise.resolve([...(firstEtape.titulaireIds ?? []), ...(firstEtape.amodiataireIds ?? [])])),
+    })),
+    Effect.bind('etapeDocuments', ({ firstEtape, demarche, titreProps }) => {
+      return getDocumentsByEtapeId(firstEtape.id, pool, userSuper, titreProps.titreTypeId, titreProps.administrationsLocales, titreProps.entreprisesTitulairesOuAmodiataires, firstEtape.typeId, {
+        demarche_type_id: demarche.demarche_type_id,
+        entreprises_lecture: demarche.entreprises_lecture,
+        public_lecture: demarche.public_lecture,
+        titre_public_lecture: false,
+      })
+    }),
+    Effect.bind('entrepriseDocuments', ({ firstEtape }) => getEntrepriseDocumentIdsByEtapeId({ titre_etape_id: firstEtape.id }, pool, userSuper)),
+    Effect.bind('etapeAvis', ({ firstEtape, demarche, titreProps }) =>
+      getEtapeAvisLargeObjectIdsByEtapeId(
+        firstEtape.id,
+        pool,
+        userSuper,
+        titreProps.titreTypeId,
+        titreProps.administrationsLocales,
+        titreProps.entreprisesTitulairesOuAmodiataires,
+        firstEtape.typeId,
+        {
+          demarche_type_id: demarche.demarche_type_id,
+          entreprises_lecture: demarche.entreprises_lecture,
+          public_lecture: demarche.public_lecture,
+          titre_public_lecture: false,
+        }
+      )
+    ),
+    Effect.bind('flattenFirstEtape', ({ firstEtape }) => iTitreEtapeToFlattenEtape(firstEtape)),
+    Effect.map(({ demarche, firstEtape, flattenFirstEtape, etapeDocuments, entrepriseDocuments, etapeAvis }) => {
+      const isFirstEtapeComplete = isEtapeComplete(
+        flattenFirstEtape,
+        demarche.titre_type_id,
+        demarcheId,
+        demarche.demarche_type_id,
+        etapeDocuments,
+        entrepriseDocuments,
+        firstEtape.sdomZones,
+        isNotNullNorUndefined(firstEtape.communes) ? firstEtape.communes.map(({ id }) => id) : [],
+        etapeAvis,
+        userSuper,
+        firstEtapeDateValidator.parse(flattenFirstEtape.date)
+      )
+
+      return {
+        titre_slug: demarche.titre_slug,
+        demarche_id: demarche.demarche_id,
+        etape_id: firstEtape.id,
+        etape_date: firstEtape.date,
+        titre_id: demarche.titre_id,
+        errors: isFirstEtapeComplete.valid ? [] : isFirstEtapeComplete.errors,
+      }
+    }),
+    Effect.catchAll(caminoError => {
+      return Effect.succeed({
+        titre_slug: null,
+        demarche_id: demarcheId,
+        etape_id: null,
+        etape_date: null,
+        titre_id: null,
+        errors: [caminoError.message],
+      })
+    })
+  )
+}
+
+const multiPolygonWith4Points: FeatureMultiPolygon = {
+  type: 'Feature',
+  properties: {},
+  geometry: {
+    type: 'MultiPolygon',
+    coordinates: [
+      [
+        [
+          [-53.16822754488772, 5.02935254143807],
+          [-53.15913163720232, 5.029382753429523],
+          [-53.15910186841349, 5.020342601941031],
+          [-53.168197650929095, 5.02031244452273],
+          [-53.16822754488772, 5.02935254143807],
+        ],
+      ],
+    ],
+  },
+}
+
+const etapeIdVideError = "Pas d'id d'étape" as const
+const titreIdVideError = "Pas d'id de titre" as const
+const upsertFailError = "Échec de mise à jour de l'étape" as const
+const dateVideError = "Pas de date d'étape" as const
+type FixProlongationErrors = typeof etapeIdVideError | typeof titreIdVideError | typeof upsertFailError | typeof dateVideError
+const fixProlongation = (checkedProlongation: CheckedProlongation): Effect.Effect<null, CaminoError<FixProlongationErrors>> => {
+  return Effect.Do.pipe(
+    Effect.bind('etapeId', () =>
+      pipe(
+        Effect.succeed(checkedProlongation.etape_id),
+        Effect.filterOrFail(
+          (etapeId): etapeId is EtapeId => isNotNullNorUndefined(etapeId),
+          () => ({ message: etapeIdVideError })
+        )
+      )
+    ),
+    Effect.bind('titreId', () =>
+      pipe(
+        Effect.succeed(checkedProlongation.titre_id),
+        Effect.filterOrFail(
+          (titreId): titreId is TitreId => isNotNullNorUndefined(titreId),
+          () => ({ message: titreIdVideError })
+        )
+      )
+    ),
+    Effect.bind('etapeDate', () =>
+      pipe(
+        Effect.succeed(checkedProlongation.etape_date),
+        Effect.filterOrFail(
+          (date): date is CaminoDate => isNotNullNorUndefined(date),
+          () => ({ message: dateVideError })
+        )
+      )
+    ),
+    Effect.flatMap(({ etapeId, titreId, etapeDate }) =>
+      Effect.tryPromise({
+        try: () =>
+          titreEtapeUpsert(
+            {
+              id: etapeId,
+              titreDemarcheId: checkedProlongation.demarche_id,
+              date: etapeDate,
+              substances: [SUBSTANCES_FISCALES_IDS.or],
+              titulaireIds: [entrepriseIdValidator.parse('fr-325313195')],
+              surface: km2Validator.parse(42),
+              geojson4326Perimetre: multiPolygonWith4Points,
+              geojsonOriginePerimetre: multiPolygonWith4Points,
+              geojsonOrigineGeoSystemeId: '4326',
+            },
+            userSuper,
+            titreId
+          ),
+        catch: error => ({ message: upsertFailError, extra: error }),
+      })
+    ),
+    Effect.map(() => null),
+    Effect.catchAll((error) => {
+      console.log(error)
+      return Effect.succeed(null)
+    })
+  )
+}
+
+const toPercentage = (percentage: number): string => {
+  return `${Math.round(percentage * 10000) / 100} %`
+}
+
+export const checkProlongations = (pool: Pool): Effect.Effect<CheckedProlongation[], CaminoError<CheckOnePrologationErrors | FixProlongationErrors>> => {
+  return pipe(
+    getDemarches(pool),
+    Effect.flatMap(demarches => {
+      const initialAcc: CheckedProlongation[] = []
+      return Effect.reduce(
+        demarches.map(({ id }) => id),
+        initialAcc,
+        (acc, demarcheId) => {
+          return pipe(
+            checkOneProlongation(pool, demarcheId),
+            Effect.tap(checkedProlongation => {
+              if (isNullOrUndefinedOrEmpty(checkedProlongation.errors) || checkedProlongation.errors.includes(demarchePasProlongationError)) {
+                return Effect.succeed(checkedProlongation)
+              }
+
+              return fixProlongation(checkedProlongation)
+            }),
+            Effect.map(checkedProlongation => {
+              if (isNotNullNorUndefinedNorEmpty(checkedProlongation.errors) && !checkedProlongation.errors.includes(demarchePasProlongationError)) {
+                const link = isNotNullNorUndefined(checkedProlongation.titre_slug)
+                  ? `http://localhost:4180/titres/${checkedProlongation.titre_slug}`
+                  : `http://localhost:4180/demarches/${checkedProlongation.demarche_id}`
+                console.log(`${link} : ${checkedProlongation.errors.join(' | ')}`)
+              }
+
+              return [...acc, checkedProlongation]
+            })
+          )
+        }
+      )
+    }),
+    Effect.tap(checkedProlongation => {
+      const prolongations = checkedProlongation.filter(({ errors }) => !errors.includes(demarchePasProlongationError))
+      const prolongationsEnVrac = prolongations.filter(({ errors }) => errors.length > 0)
+      const prolongationsAvecDureeManquante = prolongations.filter(({ errors }) => errors.some(error => error.includes('la durée est obligatoire')))
+
+      const prolongationsParType = prolongationsEnVrac.reduce<{ [key: string]: number }>((acc, prolongation) => {
+        const domaine = isNotNullNorUndefined(prolongation.titre_slug) ? `${prolongation.titre_slug.slice(2, 4)}${ prolongation.titre_slug.slice(0, 1)}` : 'inconnu'
+        if (isNullOrUndefined(acc[domaine])) {
+          acc[domaine] = 1
+        } else {
+          acc[domaine] += 1
+        }
+
+        return acc
+      }, {})
+      console.log(`Prolongations en vrac : ${prolongationsEnVrac.length} (${toPercentage(prolongationsEnVrac.length / prolongations.length)})`)
+      console.log(JSON.stringify(prolongationsParType, null, 2))
+      console.log(`Prolongations sans durée : ${prolongationsAvecDureeManquante.length} (${toPercentage(prolongationsAvecDureeManquante.length / prolongationsEnVrac.length)})`)
+    })
+  )
+}
diff --git a/packages/api/src/scripts/check-prolongations.ts b/packages/api/src/scripts/check-prolongations.ts
new file mode 100644
index 000000000..fa142e25b
--- /dev/null
+++ b/packages/api/src/scripts/check-prolongations.ts
@@ -0,0 +1,14 @@
+import '../init'
+import pg from 'pg'
+import { config } from '../config'
+import { checkProlongations } from '../business/check-prolongations'
+import { Effect } from 'effect'
+
+const pool = new pg.Pool({
+  host: config().PGHOST,
+  user: config().PGUSER,
+  password: config().PGPASSWORD,
+  database: config().PGDATABASE,
+})
+
+Effect.runPromiseExit(checkProlongations(pool)).catch(console.error)
-- 
GitLab


From 7259a28080c46b62eebf3fdda51df3030b9bf9fe Mon Sep 17 00:00:00 2001
From: Anis Safine Laget <anis.safine@beta.gouv.fr>
Date: Mon, 17 Mar 2025 11:53:36 +0100
Subject: [PATCH 2/4] =?UTF-8?q?met=20=C3=A0=20jour=20avec=20les=20bonnes?=
 =?UTF-8?q?=20donn=C3=A9es?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../api/src/business/check-prolongations.ts   | 152 +++++++++---------
 1 file changed, 78 insertions(+), 74 deletions(-)

diff --git a/packages/api/src/business/check-prolongations.ts b/packages/api/src/business/check-prolongations.ts
index 9e80e263a..f64959723 100644
--- a/packages/api/src/business/check-prolongations.ts
+++ b/packages/api/src/business/check-prolongations.ts
@@ -5,7 +5,7 @@ import { CaminoError } from 'camino-common/src/zod-tools'
 import { DemarcheId } from 'camino-common/src/demarche'
 import { isEtapeComplete } from 'camino-common/src/permissions/titres-etapes'
 import { userSuper } from '../database/user-super'
-import { CaminoDate, firstEtapeDateValidator } from 'camino-common/src/date'
+import { firstEtapeDateValidator } from 'camino-common/src/date'
 import { titreEtapeGet, titreEtapeUpsert } from '../database/queries/titres-etapes'
 import { isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty, isNullOrUndefined, isNullOrUndefinedOrEmpty, memoize, toSorted } from 'camino-common/src/typescript-tools'
 import { iTitreEtapeToFlattenEtape, TitreEtapeToFlattenEtapeErrors } from '../api/_format/titres-etapes'
@@ -22,16 +22,14 @@ import { ZodUnparseable } from '../tools/fp-tools'
 import { TitreId, TitreSlug } from 'camino-common/src/validators/titres'
 import { EtapeId } from 'camino-common/src/etape'
 import { isDemarcheTypeProlongations } from 'camino-common/src/static/demarchesTypes'
-import { SUBSTANCES_FISCALES_IDS } from 'camino-common/src/static/substancesFiscales'
-import { entrepriseIdValidator } from 'camino-common/src/entreprise'
-import { km2Validator } from 'camino-common/src/number'
-import { FeatureMultiPolygon } from 'camino-common/src/perimetre'
+import { getMostRecentValuePropFromEtapeFondamentaleValide, TitreGetDemarche } from 'camino-common/src/titres'
+import { isDemarcheStatutNonStatue, isDemarcheStatutNonValide } from 'camino-common/src/static/demarchesStatuts'
+import { getTitre } from '../api/rest/titres.queries'
 
 type CheckedProlongation = {
   titre_slug: TitreSlug | null
   demarche_id: DemarcheId
   etape_id: EtapeId | null
-  etape_date: CaminoDate | null
   titre_id: TitreId | null
   errors: string[]
 }
@@ -125,7 +123,6 @@ const checkOneProlongation = (pool: Pool, demarcheId: DemarcheId): Effect.Effect
         titre_slug: demarche.titre_slug,
         demarche_id: demarche.demarche_id,
         etape_id: firstEtape.id,
-        etape_date: firstEtape.date,
         titre_id: demarche.titre_id,
         errors: isFirstEtapeComplete.valid ? [] : isFirstEtapeComplete.errors,
       }
@@ -135,7 +132,6 @@ const checkOneProlongation = (pool: Pool, demarcheId: DemarcheId): Effect.Effect
         titre_slug: null,
         demarche_id: demarcheId,
         etape_id: null,
-        etape_date: null,
         titre_id: null,
         errors: [caminoError.message],
       })
@@ -143,77 +139,85 @@ const checkOneProlongation = (pool: Pool, demarcheId: DemarcheId): Effect.Effect
   )
 }
 
-const multiPolygonWith4Points: FeatureMultiPolygon = {
-  type: 'Feature',
-  properties: {},
-  geometry: {
-    type: 'MultiPolygon',
-    coordinates: [
-      [
-        [
-          [-53.16822754488772, 5.02935254143807],
-          [-53.15913163720232, 5.029382753429523],
-          [-53.15910186841349, 5.020342601941031],
-          [-53.168197650929095, 5.02031244452273],
-          [-53.16822754488772, 5.02935254143807],
-        ],
-      ],
-    ],
-  },
-}
-
-const etapeIdVideError = "Pas d'id d'étape" as const
 const titreIdVideError = "Pas d'id de titre" as const
 const upsertFailError = "Échec de mise à jour de l'étape" as const
-const dateVideError = "Pas de date d'étape" as const
-type FixProlongationErrors = typeof etapeIdVideError | typeof titreIdVideError | typeof upsertFailError | typeof dateVideError
-const fixProlongation = (checkedProlongation: CheckedProlongation): Effect.Effect<null, CaminoError<FixProlongationErrors>> => {
+const titreGetError = "Impossible de fetch le titre" as const
+const titreGetIsNullError = "Titre introuvable" as const
+const etapeIntrouvableError = "Étape introuvable dans le titre" as const
+const demarcheListIsEmptyError = "Liste des démarches héritables vide" as const
+type FixProlongationErrors = typeof titreIdVideError | typeof upsertFailError | typeof titreGetError | typeof titreGetIsNullError | typeof demarcheListIsEmptyError | typeof etapeIntrouvableError
+const fixProlongation = (pool: Pool, checkedProlongation: CheckedProlongation): Effect.Effect<null, CaminoError<FixProlongationErrors>> => {
   return Effect.Do.pipe(
-    Effect.bind('etapeId', () =>
-      pipe(
-        Effect.succeed(checkedProlongation.etape_id),
-        Effect.filterOrFail(
-          (etapeId): etapeId is EtapeId => isNotNullNorUndefined(etapeId),
-          () => ({ message: etapeIdVideError })
-        )
+    Effect.bind('titre', () => pipe(
+      Effect.succeed(checkedProlongation.titre_id),
+      Effect.filterOrFail(
+        (titreId) => isNotNullNorUndefined(titreId),
+        () => ({ message: titreIdVideError })
+      ),
+      Effect.flatMap((titreId) => Effect.tryPromise({
+        try: () => getTitre(pool, userSuper, titreId),
+        catch: (error) => ({ message: titreGetError, extra: error })
+      })),
+      Effect.filterOrFail(
+        (titre): titre is NonNullable<typeof titre> => isNotNullNorUndefined(titre),
+        (error) => ({ message: titreGetIsNullError, extra: error })
       )
-    ),
-    Effect.bind('titreId', () =>
-      pipe(
-        Effect.succeed(checkedProlongation.titre_id),
-        Effect.filterOrFail(
-          (titreId): titreId is TitreId => isNotNullNorUndefined(titreId),
-          () => ({ message: titreIdVideError })
-        )
+    )),
+    Effect.bind('etape', ({ titre }) => pipe(
+      Effect.succeed(titre.demarches.find(({ id }) => id === checkedProlongation.demarche_id)),
+      Effect.filterOrFail(
+        (demarche): demarche is NonNullable<typeof demarche> => isNotNullNorUndefined(checkedProlongation.demarche_id) && isNotNullNorUndefined(demarche),
+        () => ({ message: etapeIntrouvableError, extra: 'Démarche introuvable' })
+      ),
+      Effect.map((demarche) => demarche.etapes.find(({ id }) => id === checkedProlongation.etape_id)),
+      Effect.filterOrFail(
+        (etape): etape is NonNullable<typeof etape> => isNotNullNorUndefined(checkedProlongation.etape_id) && isNotNullNorUndefined(etape),
+        () => ({ message: etapeIntrouvableError, extra: 'Étape introuvable' })
       )
-    ),
-    Effect.bind('etapeDate', () =>
-      pipe(
-        Effect.succeed(checkedProlongation.etape_date),
-        Effect.filterOrFail(
-          (date): date is CaminoDate => isNotNullNorUndefined(date),
-          () => ({ message: dateVideError })
-        )
+    )),
+    Effect.bind('propsHeritees', ({ titre }) =>
+      Effect.Do.pipe(
+        Effect.bind('demarches', () => {
+          const demarches: TitreGetDemarche[] = []
+          for (const demarche of toSorted([...titre.demarches], (a, b) => a.ordre - b.ordre)) {
+            if (demarche.id === checkedProlongation.demarche_id) {
+              return Effect.succeed([...demarches, demarche])
+            } else if (!isDemarcheStatutNonStatue(demarche.demarche_statut_id) && !isDemarcheStatutNonValide(demarche.demarche_statut_id)) {
+              demarches.push(demarche)
+            }
+          }
+
+          return Effect.fail({ message: demarcheListIsEmptyError })
+        }),
+        Effect.map(({ demarches }) => {
+          return {
+            substances: getMostRecentValuePropFromEtapeFondamentaleValide('substances', demarches),
+            titulaireIds: getMostRecentValuePropFromEtapeFondamentaleValide('titulaireIds', demarches),
+            perimetre: getMostRecentValuePropFromEtapeFondamentaleValide('perimetre', demarches),
+          }
+        })
       )
     ),
-    Effect.flatMap(({ etapeId, titreId, etapeDate }) =>
+    Effect.flatMap(({ titre, etape, propsHeritees }) =>
       Effect.tryPromise({
-        try: () =>
-          titreEtapeUpsert(
+        try: () => {
+          console.log(`Trying to fix http://localhost:4180/etapes/${etape.id}`)
+          return titreEtapeUpsert(
             {
-              id: etapeId,
+              id: etape.id,
               titreDemarcheId: checkedProlongation.demarche_id,
-              date: etapeDate,
-              substances: [SUBSTANCES_FISCALES_IDS.or],
-              titulaireIds: [entrepriseIdValidator.parse('fr-325313195')],
-              surface: km2Validator.parse(42),
-              geojson4326Perimetre: multiPolygonWith4Points,
-              geojsonOriginePerimetre: multiPolygonWith4Points,
-              geojsonOrigineGeoSystemeId: '4326',
+              date: etape.date,
+              substances: 'fondamentale' in etape && isNotNullNorUndefinedNorEmpty(etape.fondamentale.substances) ? etape.fondamentale.substances : propsHeritees.substances,
+              titulaireIds: 'fondamentale' in etape && isNotNullNorUndefinedNorEmpty(etape.fondamentale.titulaireIds) ? etape.fondamentale.titulaireIds : propsHeritees.titulaireIds,
+              surface: 'fondamentale' in etape && isNotNullNorUndefined(etape.fondamentale.perimetre?.surface) ? etape.fondamentale.perimetre.surface : propsHeritees.perimetre?.surface,
+              geojson4326Perimetre: 'fondamentale' in etape && isNotNullNorUndefined(etape.fondamentale.perimetre?.geojson4326_perimetre) ? etape.fondamentale.perimetre.geojson4326_perimetre : propsHeritees.perimetre?.geojson4326_perimetre,
+              geojsonOriginePerimetre: 'fondamentale' in etape && isNotNullNorUndefined(etape.fondamentale.perimetre?.geojson_origine_perimetre) ? etape.fondamentale.perimetre.geojson_origine_perimetre : propsHeritees.perimetre?.geojson_origine_perimetre,
+              geojsonOrigineGeoSystemeId: 'fondamentale' in etape && isNotNullNorUndefined(etape.fondamentale.perimetre?.geojson_origine_geo_systeme_id) ? etape.fondamentale.perimetre.geojson_origine_geo_systeme_id : propsHeritees.perimetre?.geojson_origine_geo_systeme_id,
             },
             userSuper,
-            titreId
-          ),
+            titre.id
+          )
+        },
         catch: error => ({ message: upsertFailError, extra: error }),
       })
     ),
@@ -245,15 +249,15 @@ export const checkProlongations = (pool: Pool): Effect.Effect<CheckedProlongatio
                 return Effect.succeed(checkedProlongation)
               }
 
-              return fixProlongation(checkedProlongation)
+              return fixProlongation(pool, checkedProlongation)
             }),
             Effect.map(checkedProlongation => {
-              if (isNotNullNorUndefinedNorEmpty(checkedProlongation.errors) && !checkedProlongation.errors.includes(demarchePasProlongationError)) {
-                const link = isNotNullNorUndefined(checkedProlongation.titre_slug)
-                  ? `http://localhost:4180/titres/${checkedProlongation.titre_slug}`
-                  : `http://localhost:4180/demarches/${checkedProlongation.demarche_id}`
-                console.log(`${link} : ${checkedProlongation.errors.join(' | ')}`)
-              }
+              // if (isNotNullNorUndefinedNorEmpty(checkedProlongation.errors) && !checkedProlongation.errors.includes(demarchePasProlongationError)) {
+              //   const link = isNotNullNorUndefined(checkedProlongation.titre_slug)
+              //     ? `http://localhost:4180/titres/${checkedProlongation.titre_slug}`
+              //     : `http://localhost:4180/demarches/${checkedProlongation.demarche_id}`
+              //   console.log(`${link} : ${checkedProlongation.errors.join(' | ')}`)
+              // }
 
               return [...acc, checkedProlongation]
             })
-- 
GitLab


From 82e28cba0fe01b7b1e3ac56f4a0a1f3e449b6b20 Mon Sep 17 00:00:00 2001
From: Anis Safine Laget <anis.safine@beta.gouv.fr>
Date: Mon, 17 Mar 2025 16:21:59 +0100
Subject: [PATCH 3/4] fix ci

---
 packages/api/src/api/rest/demarches.queries.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/api/src/api/rest/demarches.queries.ts b/packages/api/src/api/rest/demarches.queries.ts
index 18f583930..9623f992d 100644
--- a/packages/api/src/api/rest/demarches.queries.ts
+++ b/packages/api/src/api/rest/demarches.queries.ts
@@ -435,7 +435,7 @@ FROM (
 ) tmp
 `
 const getDemarchesValidator = z.object({ id: demarcheIdValidator })
-export type GetDemarche = z.infer<typeof getDemarchesValidator>
+type GetDemarche = z.infer<typeof getDemarchesValidator>
 export const getDemarches = (pool: Pool): Effect.Effect<GetDemarche[], CaminoError<EffectDbQueryAndValidateErrors>> => effectDbQueryAndValidate(getDemarchesDb, {}, pool, getDemarchesValidator)
 
 const getDemarchesDb = sql<Redefine<IGetDemarchesDbQuery, {}, GetDemarche>>`
-- 
GitLab


From f69fc900d2ad51df4d8abc42746d6aead9403b3a Mon Sep 17 00:00:00 2001
From: Anis Safine Laget <anis.safine@beta.gouv.fr>
Date: Mon, 17 Mar 2025 16:23:47 +0100
Subject: [PATCH 4/4] fix ci

---
 .../api/src/business/check-prolongations.ts   | 95 +++++++++++--------
 1 file changed, 55 insertions(+), 40 deletions(-)

diff --git a/packages/api/src/business/check-prolongations.ts b/packages/api/src/business/check-prolongations.ts
index f64959723..8f378675d 100644
--- a/packages/api/src/business/check-prolongations.ts
+++ b/packages/api/src/business/check-prolongations.ts
@@ -141,40 +141,46 @@ const checkOneProlongation = (pool: Pool, demarcheId: DemarcheId): Effect.Effect
 
 const titreIdVideError = "Pas d'id de titre" as const
 const upsertFailError = "Échec de mise à jour de l'étape" as const
-const titreGetError = "Impossible de fetch le titre" as const
-const titreGetIsNullError = "Titre introuvable" as const
-const etapeIntrouvableError = "Étape introuvable dans le titre" as const
-const demarcheListIsEmptyError = "Liste des démarches héritables vide" as const
+const titreGetError = 'Impossible de fetch le titre' as const
+const titreGetIsNullError = 'Titre introuvable' as const
+const etapeIntrouvableError = 'Étape introuvable dans le titre' as const
+const demarcheListIsEmptyError = 'Liste des démarches héritables vide' as const
 type FixProlongationErrors = typeof titreIdVideError | typeof upsertFailError | typeof titreGetError | typeof titreGetIsNullError | typeof demarcheListIsEmptyError | typeof etapeIntrouvableError
 const fixProlongation = (pool: Pool, checkedProlongation: CheckedProlongation): Effect.Effect<null, CaminoError<FixProlongationErrors>> => {
   return Effect.Do.pipe(
-    Effect.bind('titre', () => pipe(
-      Effect.succeed(checkedProlongation.titre_id),
-      Effect.filterOrFail(
-        (titreId) => isNotNullNorUndefined(titreId),
-        () => ({ message: titreIdVideError })
-      ),
-      Effect.flatMap((titreId) => Effect.tryPromise({
-        try: () => getTitre(pool, userSuper, titreId),
-        catch: (error) => ({ message: titreGetError, extra: error })
-      })),
-      Effect.filterOrFail(
-        (titre): titre is NonNullable<typeof titre> => isNotNullNorUndefined(titre),
-        (error) => ({ message: titreGetIsNullError, extra: error })
+    Effect.bind('titre', () =>
+      pipe(
+        Effect.succeed(checkedProlongation.titre_id),
+        Effect.filterOrFail(
+          titreId => isNotNullNorUndefined(titreId),
+          () => ({ message: titreIdVideError })
+        ),
+        Effect.flatMap(titreId =>
+          Effect.tryPromise({
+            try: () => getTitre(pool, userSuper, titreId),
+            catch: error => ({ message: titreGetError, extra: error }),
+          })
+        ),
+        Effect.filterOrFail(
+          (titre): titre is NonNullable<typeof titre> => isNotNullNorUndefined(titre),
+          error => ({ message: titreGetIsNullError, extra: error })
+        )
       )
-    )),
-    Effect.bind('etape', ({ titre }) => pipe(
-      Effect.succeed(titre.demarches.find(({ id }) => id === checkedProlongation.demarche_id)),
-      Effect.filterOrFail(
-        (demarche): demarche is NonNullable<typeof demarche> => isNotNullNorUndefined(checkedProlongation.demarche_id) && isNotNullNorUndefined(demarche),
-        () => ({ message: etapeIntrouvableError, extra: 'Démarche introuvable' })
-      ),
-      Effect.map((demarche) => demarche.etapes.find(({ id }) => id === checkedProlongation.etape_id)),
-      Effect.filterOrFail(
-        (etape): etape is NonNullable<typeof etape> => isNotNullNorUndefined(checkedProlongation.etape_id) && isNotNullNorUndefined(etape),
-        () => ({ message: etapeIntrouvableError, extra: 'Étape introuvable' })
+    ),
+    Effect.bind('etape', ({ titre }) =>
+      pipe(
+        Effect.succeed(titre.demarches.find(({ id }) => id === checkedProlongation.demarche_id)),
+        Effect.filterOrFail(
+          (demarche): demarche is NonNullable<typeof demarche> => isNotNullNorUndefined(checkedProlongation.demarche_id) && isNotNullNorUndefined(demarche),
+          () => ({ message: etapeIntrouvableError, extra: 'Démarche introuvable' })
+        ),
+        Effect.map(demarche => demarche.etapes.find(({ id }) => id === checkedProlongation.etape_id)),
+        Effect.filterOrFail(
+          (etape): etape is NonNullable<typeof etape> => isNotNullNorUndefined(checkedProlongation.etape_id) && isNotNullNorUndefined(etape),
+          () => ({ message: etapeIntrouvableError, extra: 'Étape introuvable' })
+        )
       )
-    )),
+    ),
     Effect.bind('propsHeritees', ({ titre }) =>
       Effect.Do.pipe(
         Effect.bind('demarches', () => {
@@ -201,7 +207,7 @@ const fixProlongation = (pool: Pool, checkedProlongation: CheckedProlongation):
     Effect.flatMap(({ titre, etape, propsHeritees }) =>
       Effect.tryPromise({
         try: () => {
-          console.log(`Trying to fix http://localhost:4180/etapes/${etape.id}`)
+          console.info(`Trying to fix http://localhost:4180/etapes/${etape.id}`)
           return titreEtapeUpsert(
             {
               id: etape.id,
@@ -210,9 +216,18 @@ const fixProlongation = (pool: Pool, checkedProlongation: CheckedProlongation):
               substances: 'fondamentale' in etape && isNotNullNorUndefinedNorEmpty(etape.fondamentale.substances) ? etape.fondamentale.substances : propsHeritees.substances,
               titulaireIds: 'fondamentale' in etape && isNotNullNorUndefinedNorEmpty(etape.fondamentale.titulaireIds) ? etape.fondamentale.titulaireIds : propsHeritees.titulaireIds,
               surface: 'fondamentale' in etape && isNotNullNorUndefined(etape.fondamentale.perimetre?.surface) ? etape.fondamentale.perimetre.surface : propsHeritees.perimetre?.surface,
-              geojson4326Perimetre: 'fondamentale' in etape && isNotNullNorUndefined(etape.fondamentale.perimetre?.geojson4326_perimetre) ? etape.fondamentale.perimetre.geojson4326_perimetre : propsHeritees.perimetre?.geojson4326_perimetre,
-              geojsonOriginePerimetre: 'fondamentale' in etape && isNotNullNorUndefined(etape.fondamentale.perimetre?.geojson_origine_perimetre) ? etape.fondamentale.perimetre.geojson_origine_perimetre : propsHeritees.perimetre?.geojson_origine_perimetre,
-              geojsonOrigineGeoSystemeId: 'fondamentale' in etape && isNotNullNorUndefined(etape.fondamentale.perimetre?.geojson_origine_geo_systeme_id) ? etape.fondamentale.perimetre.geojson_origine_geo_systeme_id : propsHeritees.perimetre?.geojson_origine_geo_systeme_id,
+              geojson4326Perimetre:
+                'fondamentale' in etape && isNotNullNorUndefined(etape.fondamentale.perimetre?.geojson4326_perimetre)
+                  ? etape.fondamentale.perimetre.geojson4326_perimetre
+                  : propsHeritees.perimetre?.geojson4326_perimetre,
+              geojsonOriginePerimetre:
+                'fondamentale' in etape && isNotNullNorUndefined(etape.fondamentale.perimetre?.geojson_origine_perimetre)
+                  ? etape.fondamentale.perimetre.geojson_origine_perimetre
+                  : propsHeritees.perimetre?.geojson_origine_perimetre,
+              geojsonOrigineGeoSystemeId:
+                'fondamentale' in etape && isNotNullNorUndefined(etape.fondamentale.perimetre?.geojson_origine_geo_systeme_id)
+                  ? etape.fondamentale.perimetre.geojson_origine_geo_systeme_id
+                  : propsHeritees.perimetre?.geojson_origine_geo_systeme_id,
             },
             userSuper,
             titre.id
@@ -222,8 +237,8 @@ const fixProlongation = (pool: Pool, checkedProlongation: CheckedProlongation):
       })
     ),
     Effect.map(() => null),
-    Effect.catchAll((error) => {
-      console.log(error)
+    Effect.catchAll(error => {
+      console.info(error)
       return Effect.succeed(null)
     })
   )
@@ -256,7 +271,7 @@ export const checkProlongations = (pool: Pool): Effect.Effect<CheckedProlongatio
               //   const link = isNotNullNorUndefined(checkedProlongation.titre_slug)
               //     ? `http://localhost:4180/titres/${checkedProlongation.titre_slug}`
               //     : `http://localhost:4180/demarches/${checkedProlongation.demarche_id}`
-              //   console.log(`${link} : ${checkedProlongation.errors.join(' | ')}`)
+              //   console.info(`${link} : ${checkedProlongation.errors.join(' | ')}`)
               // }
 
               return [...acc, checkedProlongation]
@@ -271,7 +286,7 @@ export const checkProlongations = (pool: Pool): Effect.Effect<CheckedProlongatio
       const prolongationsAvecDureeManquante = prolongations.filter(({ errors }) => errors.some(error => error.includes('la durée est obligatoire')))
 
       const prolongationsParType = prolongationsEnVrac.reduce<{ [key: string]: number }>((acc, prolongation) => {
-        const domaine = isNotNullNorUndefined(prolongation.titre_slug) ? `${prolongation.titre_slug.slice(2, 4)}${ prolongation.titre_slug.slice(0, 1)}` : 'inconnu'
+        const domaine = isNotNullNorUndefined(prolongation.titre_slug) ? `${prolongation.titre_slug.slice(2, 4)}${prolongation.titre_slug.slice(0, 1)}` : 'inconnu'
         if (isNullOrUndefined(acc[domaine])) {
           acc[domaine] = 1
         } else {
@@ -280,9 +295,9 @@ export const checkProlongations = (pool: Pool): Effect.Effect<CheckedProlongatio
 
         return acc
       }, {})
-      console.log(`Prolongations en vrac : ${prolongationsEnVrac.length} (${toPercentage(prolongationsEnVrac.length / prolongations.length)})`)
-      console.log(JSON.stringify(prolongationsParType, null, 2))
-      console.log(`Prolongations sans durée : ${prolongationsAvecDureeManquante.length} (${toPercentage(prolongationsAvecDureeManquante.length / prolongationsEnVrac.length)})`)
+      console.info(`Prolongations en vrac : ${prolongationsEnVrac.length} (${toPercentage(prolongationsEnVrac.length / prolongations.length)})`)
+      console.info(JSON.stringify(prolongationsParType, null, 2))
+      console.info(`Prolongations sans durée : ${prolongationsAvecDureeManquante.length} (${toPercentage(prolongationsAvecDureeManquante.length / prolongationsEnVrac.length)})`)
     })
   )
 }
-- 
GitLab