From a6ee97fcad4aae16652516618901f5958085d90e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?BITARD=20Micha=C3=ABl?= <michael.bitard@beta.gouv.fr>
Date: Thu, 10 Apr 2025 12:46:20 +0000
Subject: [PATCH] =?UTF-8?q?fix(instructions):=20peut=20finaliser=20une=20?=
 =?UTF-8?q?=C3=A9tape=20(pub/pnm-public/camino!1699)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/api/src/api/rest/etapes.ts           |  1 +
 .../titre-demarche-etat-validate.test.ts      | 58 +++++++++++++++++
 .../titre-demarche-etat-validate.ts           | 16 ++++-
 packages/common/src/titres.test.ts            | 62 +++++++++++++++----
 packages/common/src/titres.ts                 | 21 +++++++
 .../components/demarche/demarche-etape.tsx    | 13 ++--
 6 files changed, 150 insertions(+), 21 deletions(-)

diff --git a/packages/api/src/api/rest/etapes.ts b/packages/api/src/api/rest/etapes.ts
index 209ab4882..56b44029e 100644
--- a/packages/api/src/api/rest/etapes.ts
+++ b/packages/api/src/api/rest/etapes.ts
@@ -1079,6 +1079,7 @@ export const deposeEtape: RestNewPutCall<'/rest/etapes/:etapeId/depot'> = (rootP
             {
               fields: {
                 titre: { pointsEtape: { id: {} }, titulairesEtape: { id: {} }, amodiatairesEtape: { id: {} } },
+                etapes: { id: {} },
               },
             },
             userSuper
diff --git a/packages/api/src/business/validations/titre-demarche-etat-validate.test.ts b/packages/api/src/business/validations/titre-demarche-etat-validate.test.ts
index 5d4d48f48..97e529467 100644
--- a/packages/api/src/business/validations/titre-demarche-etat-validate.test.ts
+++ b/packages/api/src/business/validations/titre-demarche-etat-validate.test.ts
@@ -672,6 +672,64 @@ describe('titreDemarcheUpdatedEtatValidate', () => {
 })
 
 describe('getPossiblesEtapesTypes', () => {
+  test("bug prod edition d'une étape quand une étape de même type est en brouillon", () => {
+    const rcoId = newEtapeId('rcoId')
+    const etapes: Pick<ITitreEtape, 'typeId' | 'date' | 'isBrouillon' | 'id' | 'ordre' | 'statutId' | 'communes'>[] = [
+      { typeId: 'rco', date: toCaminoDate('2025-04-10'), isBrouillon: ETAPE_IS_BROUILLON, id: newEtapeId(), ordre: 18, statutId: 'fai', communes: [] },
+      { typeId: 'men', date: toCaminoDate('2018-12-28'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 3, statutId: 'fai', communes: [] },
+      { typeId: 'spp', date: toCaminoDate('2019-02-12'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 4, statutId: 'fai', communes: [] },
+      { typeId: 'mco', date: toCaminoDate('2019-04-29'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 5, statutId: 'fai', communes: [] },
+      {
+        typeId: 'mfr',
+        date: toCaminoDate('2018-12-20'),
+        isBrouillon: ETAPE_IS_NOT_BROUILLON,
+        id: newEtapeId(),
+        ordre: 1,
+        statutId: 'fai',
+        communes: [{ id: toCommuneId('67001') }],
+      },
+      {
+        typeId: 'mod',
+        date: toCaminoDate('2018-12-21'),
+        isBrouillon: ETAPE_IS_NOT_BROUILLON,
+        id: newEtapeId(),
+        ordre: 2,
+        statutId: 'fai',
+        communes: [{ id: toCommuneId('67001') }],
+      },
+      { typeId: 'rco', date: toCaminoDate('2019-06-05'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: rcoId, ordre: 6, statutId: 'fai', communes: [] },
+      { typeId: 'mcr', date: toCaminoDate('2019-07-10'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 7, statutId: 'fav', communes: [] },
+      { typeId: 'asc', date: toCaminoDate('2019-07-17'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 8, statutId: 'fai', communes: [] },
+      { typeId: 'adc', date: toCaminoDate('2019-07-17'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 9, statutId: 'fai', communes: [] },
+      { typeId: 'anf', date: toCaminoDate('2019-07-19'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 10, statutId: 'ter', communes: [] },
+      { typeId: 'apd', date: toCaminoDate('2020-01-13'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 11, statutId: 'fav', communes: [] },
+      { typeId: 'app', date: toCaminoDate('2020-09-01'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 12, statutId: 'fav', communes: [] },
+      { typeId: 'ppu', date: toCaminoDate('2020-10-05'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 13, statutId: 'ter', communes: [] },
+      { typeId: 'rco', date: toCaminoDate('2023-09-25'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 14, statutId: 'fai', communes: [] },
+      { typeId: 'ppu', date: toCaminoDate('2024-01-15'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 15, statutId: 'ter', communes: [] },
+      { typeId: 'apd', date: toCaminoDate('2024-03-19'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 16, statutId: 'fre', communes: [] },
+      { typeId: 'app', date: toCaminoDate('2024-04-08'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 17, statutId: 'def', communes: [] },
+    ]
+    const tested = getPossiblesEtapesTypes(
+      undefined,
+      TITRES_TYPES_IDS.PERMIS_EXCLUSIF_DE_RECHERCHES_METAUX,
+      DEMARCHES_TYPES_IDS.Octroi,
+      ETAPES_TYPES.receptionDeComplements,
+      rcoId,
+      toCaminoDate('2019-06-06'),
+      etapes
+    )
+
+    expect(tested.rco).toMatchInlineSnapshot(`
+      {
+        "etapeStatutIds": [
+          "fai",
+        ],
+        "mainStep": false,
+      }
+    `)
+  })
+
   test('peut déplacer le Rapport et avis de la DREAL avant la Consultation du public pour les Octrois de Permis exclusif de recherches ', () => {
     const apdId = newEtapeId('apiId')
     const etapes: TitreEtapeForMachine[] = [
diff --git a/packages/api/src/business/validations/titre-demarche-etat-validate.ts b/packages/api/src/business/validations/titre-demarche-etat-validate.ts
index 611b6d24b..74b86d705 100644
--- a/packages/api/src/business/validations/titre-demarche-etat-validate.ts
+++ b/packages/api/src/business/validations/titre-demarche-etat-validate.ts
@@ -5,7 +5,7 @@ import type { ITitreEtape } from '../../types'
 import { Etape, TitreEtapeForMachine, titreEtapeForMachineValidator, toMachineEtapes } from '../rules-demarches/machine-common'
 import { demarcheEnregistrementDemandeDateFind, DemarcheId } from 'camino-common/src/demarche'
 import { DemarchesTypes, DemarcheTypeId } from 'camino-common/src/static/demarchesTypes'
-import { ETAPE_IS_BROUILLON, EtapeId, EtapeTypeEtapeStatutWithMainStep } from 'camino-common/src/etape'
+import { ETAPE_IS_BROUILLON, ETAPE_IS_NOT_BROUILLON, EtapeId, EtapeTypeEtapeStatutWithMainStep } from 'camino-common/src/etape'
 import { TitreTypeId } from 'camino-common/src/static/titresTypes'
 import { CaminoMachines, machineFind } from '../rules-demarches/machines'
 import { CaminoDate } from 'camino-common/src/date'
@@ -180,7 +180,19 @@ export const getPossiblesEtapesTypes = (
   }
 
   // On ne peut pas avoir 2 fois le même type d'étape en brouillon
-  const etapeTypeIdsInBrouillon = demarcheEtapes.filter(({ isBrouillon, id }) => id !== etapeId && isBrouillon === ETAPE_IS_BROUILLON).map(({ typeId }) => typeId)
+  const etapeTypeIdsInBrouillon = demarcheEtapes
+    .filter(({ isBrouillon, id }) => {
+      if (isNullOrUndefined(etapeId)) {
+        return isBrouillon === ETAPE_IS_BROUILLON
+      } else {
+        const etape = demarcheEtapes.find(myEtape => myEtape.id === etapeId)
+        if (etape?.isBrouillon === ETAPE_IS_NOT_BROUILLON) {
+          return false
+        }
+        return id !== etapeId && isBrouillon === ETAPE_IS_BROUILLON
+      }
+    })
+    .map(({ typeId }) => typeId)
 
   for (const etapeTypeIdInBrouillon of etapeTypeIdsInBrouillon) {
     delete etapesTypes[etapeTypeIdInBrouillon]
diff --git a/packages/common/src/titres.test.ts b/packages/common/src/titres.test.ts
index 63284fbbc..bfcb78a21 100644
--- a/packages/common/src/titres.test.ts
+++ b/packages/common/src/titres.test.ts
@@ -1,12 +1,17 @@
 import { describe, expect, test } from 'vitest'
-import { TitrePropTitreEtapeFindDemarcheEtape, createAutomaticallyEtapeWhenCreatingTitre, getMostRecentValuePropFromEtapeFondamentaleValide } from './titres'
+import {
+  TitrePropTitreEtapeFindDemarcheEtape,
+  createAutomaticallyEtapeWhenCreatingTitre,
+  getMostRecentValuePropFromEtapeFondamentale,
+  getMostRecentValuePropFromEtapeFondamentaleValide,
+} from './titres'
 import { toCaminoDate } from './date'
 import { entrepriseIdValidator } from './entreprise'
 import { ETAPE_IS_BROUILLON, ETAPE_IS_NOT_BROUILLON } from './etape'
 import { testBlankUser } from './tests-utils'
 import { ETAPES_TYPES } from './static/etapesTypes'
 import { ETAPES_STATUTS } from './static/etapesStatuts'
-describe('getMostRecentValuePropFromEtapeFondamentaleValide', () => {
+describe('getMostRecentValuePropFromEtapeFondamentaleValide et non valide', () => {
   test("retourne le titulaire de la demande même si elle est en brouillon, si elle est l'unique étape", () => {
     const acg: TitrePropTitreEtapeFindDemarcheEtape = {
       etape_type_id: ETAPES_TYPES.saisinesEtAvisCGE_AE,
@@ -29,15 +34,21 @@ describe('getMostRecentValuePropFromEtapeFondamentaleValide', () => {
       is_brouillon: ETAPE_IS_BROUILLON,
       ordre: 1,
     }
-
-    expect(
-      getMostRecentValuePropFromEtapeFondamentaleValide('titulaireIds', [
+    const testedValide = getMostRecentValuePropFromEtapeFondamentaleValide('titulaireIds', [
+      {
+        etapes: [acg, mfr],
+        ordre: 1,
+      },
+    ])
+    expect(testedValide).toStrictEqual(mfr.fondamentale.titulaireIds)
+    expect(testedValide).toStrictEqual(
+      getMostRecentValuePropFromEtapeFondamentale('titulaireIds', [
         {
           etapes: [acg, mfr],
           ordre: 1,
         },
       ])
-    ).toStrictEqual(mfr.fondamentale.titulaireIds)
+    )
   })
   test('retourne le dernier titulaire même si les étapes ne sont pas dans le bon ordre', () => {
     const dpu: TitrePropTitreEtapeFindDemarcheEtape = {
@@ -72,8 +83,19 @@ describe('getMostRecentValuePropFromEtapeFondamentaleValide', () => {
       ordre: 1,
     }
 
-    expect(
-      getMostRecentValuePropFromEtapeFondamentaleValide('titulaireIds', [
+    const testedValid = getMostRecentValuePropFromEtapeFondamentaleValide('titulaireIds', [
+      {
+        etapes: [dpu, dex],
+        ordre: 1,
+      },
+      {
+        etapes: [],
+        ordre: 2,
+      },
+    ])
+    expect(testedValid).toStrictEqual(dpu.fondamentale.titulaireIds)
+    expect(testedValid).toStrictEqual(
+      getMostRecentValuePropFromEtapeFondamentale('titulaireIds', [
         {
           etapes: [dpu, dex],
           ordre: 1,
@@ -83,20 +105,28 @@ describe('getMostRecentValuePropFromEtapeFondamentaleValide', () => {
           ordre: 2,
         },
       ])
-    ).toStrictEqual(dpu.fondamentale.titulaireIds)
+    )
 
-    expect(
-      getMostRecentValuePropFromEtapeFondamentaleValide('titulaireIds', [
+    const testedValidInverse = getMostRecentValuePropFromEtapeFondamentaleValide('titulaireIds', [
+      {
+        etapes: [dex, dpu],
+        ordre: 1,
+      },
+    ])
+    expect(testedValidInverse).toStrictEqual(dpu.fondamentale.titulaireIds)
+    expect(testedValidInverse).toStrictEqual(
+      getMostRecentValuePropFromEtapeFondamentale('titulaireIds', [
         {
           etapes: [dex, dpu],
           ordre: 1,
         },
       ])
-    ).toStrictEqual(dpu.fondamentale.titulaireIds)
+    )
   })
 
   test("retourne null s'il n'y a aucune étape fondamentale", () => {
     expect(getMostRecentValuePropFromEtapeFondamentaleValide('titulaireIds', [])).toBe(null)
+    expect(getMostRecentValuePropFromEtapeFondamentale('titulaireIds', [])).toBe(null)
   })
 
   test('ignore une étape si sa propriété est null', () => {
@@ -140,6 +170,14 @@ describe('getMostRecentValuePropFromEtapeFondamentaleValide', () => {
         },
       ])
     ).toStrictEqual(240)
+    expect(
+      getMostRecentValuePropFromEtapeFondamentale('duree', [
+        {
+          etapes: [dpu, dex],
+          ordre: 1,
+        },
+      ])
+    ).toStrictEqual(null)
   })
 })
 
diff --git a/packages/common/src/titres.ts b/packages/common/src/titres.ts
index fa593498f..860550f80 100644
--- a/packages/common/src/titres.ts
+++ b/packages/common/src/titres.ts
@@ -153,6 +153,27 @@ export const getMostRecentValuePropFromEtapeFondamentaleValide = <
 
   return null
 }
+export const getMostRecentValuePropFromEtapeFondamentale = <
+  P extends 'titulaireIds' | 'amodiataireIds' | 'perimetre' | 'substances' | 'duree',
+  F extends Pick<DemarcheEtapeFondamentale, 'etape_statut_id' | 'etape_type_id' | 'ordre' | 'fondamentale' | 'is_brouillon'>,
+  NF extends Pick<DemarcheEtapeNonFondamentale, 'etape_statut_id' | 'etape_type_id' | 'ordre' | 'is_brouillon'>,
+>(
+  propId: P,
+  titreDemarches: TitrePropTitreEtapeFindDemarche<F | NF>[]
+): DemarcheEtapeFondamentale['fondamentale'][P] | null => {
+  const titreDemarchesDesc: TitrePropTitreEtapeFindDemarche<F | NF>[] = [...titreDemarches].sort((a, b) => b.ordre - a.ordre)
+
+  for (const titreDemarche of titreDemarchesDesc) {
+    const titreEtapeDesc = [...titreDemarche.etapes].sort((a, b) => b.ordre - a.ordre).filter((etape): etape is F => 'fondamentale' in etape)
+    for (const titreEtape of titreEtapeDesc) {
+      if (isFondamentalesStatutOk(titreEtape.etape_statut_id) && (titreEtape.is_brouillon === ETAPE_IS_NOT_BROUILLON || titreEtapeDesc.length === 1)) {
+        return titreEtape.fondamentale[propId]
+      }
+    }
+  }
+
+  return null
+}
 
 export const getDemarcheByIdOrSlugValidator = z.object({
   demarche_id: demarcheIdValidator,
diff --git a/packages/ui/src/components/demarche/demarche-etape.tsx b/packages/ui/src/components/demarche/demarche-etape.tsx
index ce2539487..073fdde0a 100644
--- a/packages/ui/src/components/demarche/demarche-etape.tsx
+++ b/packages/ui/src/components/demarche/demarche-etape.tsx
@@ -30,7 +30,7 @@ import { RemoveEtapePopup } from './remove-etape-popup'
 import { SDOMZoneId } from 'camino-common/src/static/sdom'
 import { DeposeEtapePopup } from './depose-etape-popup'
 import { ApiClient } from '@/api/api-client'
-import { TitreGetDemarche, getMostRecentValuePropFromEtapeFondamentaleValide } from 'camino-common/src/titres'
+import { TitreGetDemarche, getMostRecentValuePropFromEtapeFondamentale } from 'camino-common/src/titres'
 import { Unites } from 'camino-common/src/static/unites'
 import { EntrepriseId, Entreprise } from 'camino-common/src/entreprise'
 import { Badge } from '../_ui/badge'
@@ -148,12 +148,11 @@ export const DemarcheEtape = defineComponent<Props>(props => {
   )
 
   const isDeposable = computed<boolean>(() => {
-    const titulaireIds = getMostRecentValuePropFromEtapeFondamentaleValide('titulaireIds', [{ ...props.demarche, ordre: 0 }])
-    const amodiataireIds = getMostRecentValuePropFromEtapeFondamentaleValide('amodiataireIds', [{ ...props.demarche, ordre: 0 }])
-    const perimetre = getMostRecentValuePropFromEtapeFondamentaleValide('perimetre', [{ ...props.demarche, ordre: 0 }])
-    const substances = getMostRecentValuePropFromEtapeFondamentaleValide('substances', [{ ...props.demarche, ordre: 0 }])
-    const duree = getMostRecentValuePropFromEtapeFondamentaleValide('duree', [{ ...props.demarche, ordre: 0 }])
-
+    const titulaireIds = getMostRecentValuePropFromEtapeFondamentale('titulaireIds', [{ ...props.demarche, ordre: 0 }])
+    const amodiataireIds = getMostRecentValuePropFromEtapeFondamentale('amodiataireIds', [{ ...props.demarche, ordre: 0 }])
+    const perimetre = getMostRecentValuePropFromEtapeFondamentale('perimetre', [{ ...props.demarche, ordre: 0 }])
+    const substances = getMostRecentValuePropFromEtapeFondamentale('substances', [{ ...props.demarche, ordre: 0 }])
+    const duree = getMostRecentValuePropFromEtapeFondamentale('duree', [{ ...props.demarche, ordre: 0 }])
     const sections = getSections(props.titre.typeId, props.demarche.demarche_type_id, props.etape.etape_type_id)
     const sortedEtapes = [...props.demarche.etapes].sort((a, b) => b.ordre - a.ordre)
     const contenu: FlattenEtape['contenu'] = {}
-- 
GitLab