From cf0596e353e4169d6ca8583a9ae6c9fe711444e3 Mon Sep 17 00:00:00 2001
From: MAUBERT Vincent <vincent.maubert@beta.gouv.fr>
Date: Mon, 30 Sep 2024 12:10:10 +0000
Subject: [PATCH] =?UTF-8?q?feat(=C3=A9tapes):=20ajoute=20un=20lien=20vers?=
 =?UTF-8?q?=20la=20derni=C3=A8re=20=C3=A9tape=20fondamentale=20sur=20chaqu?=
 =?UTF-8?q?e=20=C3=A9tape=20(pub/pnm-public/camino!1478)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 package.json                                  |   2 +-
 packages/api/eslint.config.mjs                |   8 +
 packages/api/package.json                     |   6 +-
 packages/api/src/@types/global.d.ts           |   1 +
 .../api/graphql/resolvers/titres-activites.ts |   8 +-
 .../titres-demarches.test.integration.ts      |  36 +-
 .../api/graphql/titres.test.integration.ts    |  24 +-
 .../api/src/api/rest/demarches.queries.ts     |  25 +-
 .../src/api/rest/demarches.queries.types.ts   |  15 +
 .../api/rest/demarches.test.integration.ts    |  97 +++---
 .../api/rest/entreprises.test.integration.ts  |  27 +-
 .../api/rest/etape-creer.test.integration.ts  |  34 +-
 .../src/api/rest/etapes.test.integration.ts   | 328 +++++++++---------
 .../src/api/rest/journal.test.integration.ts  |  31 +-
 packages/api/src/api/rest/perimetre.ts        |   2 +-
 packages/api/src/api/rest/rest-test-utils.ts  |  63 ++--
 packages/api/src/api/rest/titres.queries.ts   |   5 +-
 .../src/api/rest/titres.test.integration.ts   | 150 ++++----
 packages/api/src/api/rest/titres.ts           |   6 +-
 packages/api/src/business/daily.ts            |   3 +
 ...apes-areas-update.test.integration.ts.snap |   2 +
 .../titres-activites-props-update.test.ts     |  10 +-
 .../titres-activites-props-update.ts          |  29 +-
 .../processes/titres-activites-update.test.ts |  12 +-
 .../processes/titres-activites-update.ts      |  44 ++-
 .../titres-etapes-fondamentale-id-update.ts   |  50 +++
 .../api/src/business/titre-etape-update.ts    |   3 +
 ...g-and-relations-update.test.integration.ts |  74 ++--
 .../utils/titre-slug-and-relations-update.ts  |   2 +-
 .../titre-demarche-etat-validate.ts           |   4 +-
 .../api/src/database/models/titres-etapes.ts  |   8 +-
 .../queries/entreprises-etablissements.ts     |   1 +
 .../api/src/database/queries/entreprises.ts   |   8 +-
 packages/api/src/database/queries/journaux.ts |  31 +-
 .../administrations.test.integration.ts       |   2 +-
 .../permissions/titres.test.integration.ts    | 217 ++++++------
 .../titres-activites.test.integration.ts      |   2 +-
 .../src/database/queries/titres-activites.ts  |  17 +-
 .../database/queries/titres-etapes.queries.ts |  12 +-
 .../queries/titres-etapes.queries.types.ts    |  15 +
 .../api/src/database/queries/titres-etapes.ts |   4 +-
 packages/api/src/database/queries/titres.ts   |  26 +-
 ...0240925132503_add-etape-fondamentale-fk.ts |  39 +++
 packages/api/src/pg-database.ts               |   4 +-
 .../src/tools/demarches/definitions-check.ts  |  23 +-
 packages/api/src/tools/fp-tools.ts            |   2 +
 .../_utils/administrations-permissions.ts     |   7 +-
 packages/api/tests/integration-test-helper.ts |  18 +
 packages/common/src/date.ts                   |   1 +
 packages/common/src/perimetre.ts              |  21 +-
 .../common/src/permissions/journaux.test.ts   |   7 +
 .../src/permissions/titres-demarches.ts       |   4 +-
 .../src/permissions/titres-etapes.test.ts     |  51 ++-
 .../documents.test.ts                         |   6 +-
 .../documents.ts                              |  14 +-
 packages/common/src/strings.test.ts           |   2 +
 packages/common/src/typescript-tools.ts       |   7 +
 packages/common/vitest.config.ts              |   8 +-
 58 files changed, 947 insertions(+), 711 deletions(-)
 create mode 100644 packages/api/src/business/processes/titres-etapes-fondamentale-id-update.ts
 create mode 100644 packages/api/src/knex/migrations/20240925132503_add-etape-fondamentale-fk.ts
 create mode 100644 packages/api/tests/integration-test-helper.ts
 create mode 100644 packages/common/src/permissions/journaux.test.ts

diff --git a/package.json b/package.json
index 4da060bea..a6c611c40 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,7 @@
     ],
     "packages/ui/**/*.css": "prettier --write",
     "packages/api/**/*.{js,ts}": [
-      "eslint --config packages/api/eslint.config.mjs --cache --fix --max-warnings=0",
+      "eslint --config packages/api/eslint.config.mjs --cache --fix --max-warnings=0 --no-ignore",
       "prettier --write"
     ],
     "packages/api/**/*.{graphql,md}": "prettier --write",
diff --git a/packages/api/eslint.config.mjs b/packages/api/eslint.config.mjs
index 4bf64df75..99d082919 100644
--- a/packages/api/eslint.config.mjs
+++ b/packages/api/eslint.config.mjs
@@ -111,6 +111,14 @@ export default [
           message: 'dbQueryAndValidate is to be used only in .queries.ts files',
           selector: "CallExpression[callee.name='dbQueryAndValidate']",
         },
+        {
+          message: 'insertGraph is forbidden (very very bad)',
+          selector: "Identifier[name='insertGraph']",
+        },
+        {
+          message: 'upsertGraph is forbidden (very very bad)',
+          selector: "Identifier[name='upsertGraph']",
+        },
         {
           message: 'leftJoinRelation is deprecated. Use leftJoinRelated instead.',
           selector: "Identifier[name='leftJoinRelation']",
diff --git a/packages/api/package.json b/packages/api/package.json
index e0603b5b3..1bf5d7ca0 100644
--- a/packages/api/package.json
+++ b/packages/api/package.json
@@ -7,8 +7,8 @@
   "type": "module",
   "scripts": {
     "build": "tsc --incremental",
-    "daily": "node --loader ts-node/esm/transpile-only ./src/scripts/daily.ts",
-    "monthly": "node --loader ts-node/esm/transpile-only ./src/scripts/monthly.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/",
     "postdb:dump-schema": "node -e \"console.log(\\\"il faut supprimer le 'create schema public' et 'SELECT pg_catalog.set_config('search_path', '', false);'\\\")\"",
     "db:dump-schema": "pg_dump --host=localhost --username=postgres --exclude-table=knex_migrations --exclude-table=knex_migrations_lock --exclude-table=knex_migrations_id_seq --exclude-table=knex_migrations_lock_index_seq --no-owner --no-privileges --dbname=camino --schema-only --schema public --no-comments > src/knex/migrations/20230413090214_init_schema.sql",
@@ -17,7 +17,7 @@
     "db:prod-fetch": "rm -rf ./backups/* && ssh camino.beta.gouv.fr 'rm -f ~/backup.tgz && cd  /srv/backups/dump/ && tar cvzf ~/backup.tgz .' && scp camino.beta.gouv.fr:~/backup.tgz backups/ && tar xvf backups/backup.tgz --directory ./backups",
     "db:prod-fetch-without-files": "scp camino.beta.gouv.fr:/srv/backups/dump_without_files.backup ./backup_without_files.backup",
     "db:recreate": "dropdb --host=localhost --username=postgres camino && createdb --host=localhost --username=postgres camino",
-    "db:migrate": "node --loader ts-node/esm/transpile-only ./src/knex/migrate.ts",
+    "db:migrate": "node --enable-source-maps --loader ts-node/esm/transpile-only ./src/knex/migrate.ts",
     "db:add-migration": "NODE_OPTIONS='--loader ts-node/esm/transpile-only' knex migrate:make",
     "db:watch": "npx --yes --package=@pgtyped/cli pgtyped -w -c pgtyped-config.json",
     "db:check": "npx --yes --package=@pgtyped/cli pgtyped -c pgtyped-config.ci.json",
diff --git a/packages/api/src/@types/global.d.ts b/packages/api/src/@types/global.d.ts
index b33471af3..f0a84b010 100644
--- a/packages/api/src/@types/global.d.ts
+++ b/packages/api/src/@types/global.d.ts
@@ -8,6 +8,7 @@ declare global {
   interface Array<T> {
     includes<U>(_searchElement: U & (T & U extends never ? never : unknown), _fromIndex?: number): boolean
   }
+
   interface ReadonlySet<T> {
     has(value: T | unknown): boolean
   }
diff --git a/packages/api/src/api/graphql/resolvers/titres-activites.ts b/packages/api/src/api/graphql/resolvers/titres-activites.ts
index 4a7641382..4859a2968 100644
--- a/packages/api/src/api/graphql/resolvers/titres-activites.ts
+++ b/packages/api/src/api/graphql/resolvers/titres-activites.ts
@@ -1,6 +1,6 @@
 import { GraphQLResolveInfo } from 'graphql'
 
-import { Context, ITitre, ITitreActiviteColonneId } from '../../../types'
+import { Context, ITitre, ITitreActivite, ITitreActiviteColonneId } from '../../../types'
 import { ACTIVITES_STATUTS_IDS } from 'camino-common/src/static/activitesStatuts'
 
 import { titreActiviteEmailsSend } from './_titre-activite'
@@ -26,7 +26,6 @@ import {
 } from '../../rest/activites.queries'
 import { ActiviteId } from 'camino-common/src/activite'
 import { getSectionsWithValue } from 'camino-common/src/static/titresTypes_demarchesTypes_etapesTypes/sections'
-import TitresActivites from '../../../database/models/titres-activites'
 import { getUtilisateursEmailsByEntrepriseIds } from '../../../database/queries/utilisateurs.queries'
 
 /**
@@ -86,7 +85,7 @@ export const activites = async (
   },
   { user }: Context,
   info: GraphQLResolveInfo
-): Promise<{ elements: TitresActivites[]; page?: number; intervalle?: number; ordre?: 'asc' | 'desc' | null | undefined; colonne?: ITitreActiviteColonneId | null | undefined; total: number }> => {
+): Promise<{ elements: ITitreActivite[]; page?: number; intervalle?: number; ordre?: 'asc' | 'desc' | null | undefined; colonne?: ITitreActiviteColonneId | null | undefined; total: number }> => {
   try {
     if (!canReadActivites(user)) {
       return { elements: [], total: 0 }
@@ -158,7 +157,7 @@ export const activites = async (
   }
 }
 
-export const activiteDeposer = async ({ id }: { id: ActiviteId }, { user, pool }: Context, info: GraphQLResolveInfo): Promise<TitresActivites> => {
+export const activiteDeposer = async ({ id }: { id: ActiviteId }, { user, pool }: Context, info: GraphQLResolveInfo): Promise<ITitreActivite> => {
   try {
     if (!user) throw new Error('droits insuffisants')
 
@@ -177,6 +176,7 @@ export const activiteDeposer = async ({ id }: { id: ActiviteId }, { user, pool }
     }
 
     await titreActiviteUpdateQuery(activite.id, {
+      id: activite.id,
       activiteStatutId: ACTIVITES_STATUTS_IDS.DEPOSE,
       utilisateurId: user.id,
       dateSaisie: getCurrent(),
diff --git a/packages/api/src/api/graphql/titres-demarches.test.integration.ts b/packages/api/src/api/graphql/titres-demarches.test.integration.ts
index 2c689f446..c28e80a61 100644
--- a/packages/api/src/api/graphql/titres-demarches.test.integration.ts
+++ b/packages/api/src/api/graphql/titres-demarches.test.integration.ts
@@ -1,6 +1,5 @@
 import { dbManager } from '../../../tests/db-manager'
 import { graphQLCall, queryImport } from '../../../tests/_utils/index'
-import { titreCreate } from '../../database/queries/titres'
 import { titreEtapeUpsert } from '../../database/queries/titres-etapes'
 import { userSuper } from '../../database/user-super'
 import { ADMINISTRATION_IDS } from 'camino-common/src/static/administrations'
@@ -8,9 +7,10 @@ import { toCaminoDate } from 'camino-common/src/date'
 
 import { afterAll, beforeAll, afterEach, describe, test, expect, vi } from 'vitest'
 import type { Pool } from 'pg'
-import { newEtapeId } from '../../database/models/_format/id-create'
+import { newDemarcheId, newEtapeId, newTitreId } from '../../database/models/_format/id-create'
 import TitresDemarches from '../../database/models/titres-demarches'
 import { ETAPE_IS_NOT_BROUILLON } from 'camino-common/src/etape'
+import { insertTitreGraph } from '../../../tests/integration-test-helper'
 
 console.info = vi.fn()
 console.error = vi.fn()
@@ -137,22 +137,26 @@ describe('demarcheModifier', () => {
   })
 })
 
-// TODO 2024-07-31 : mettre en commun avec demarches.test.integration (dans un fichier helper)
 const demarcheCreate = async () => {
-  const titre = await titreCreate(
-    {
-      nom: 'mon titre',
-      typeId: 'arm',
-      titreStatutId: 'ind',
-      propsTitreEtapesIds: {},
-    },
-    {}
-  )
-
-  const titreDemarche = await TitresDemarches.query().insertAndFetch({ titreId: titre.id, typeId: 'oct' })
+  const titreId = newTitreId()
+  const demarcheId = newDemarcheId()
+  await insertTitreGraph({
+    id: titreId,
+    nom: 'mon titre',
+    typeId: 'arm',
+    titreStatutId: 'ind',
+    propsTitreEtapesIds: {},
+    demarches: [
+      {
+        id: demarcheId,
+        titreId,
+        typeId: 'oct',
+      },
+    ],
+  })
 
   return {
-    titreId: titre.id,
-    demarcheId: titreDemarche.id,
+    titreId,
+    demarcheId,
   }
 }
diff --git a/packages/api/src/api/graphql/titres.test.integration.ts b/packages/api/src/api/graphql/titres.test.integration.ts
index 4284c6e0f..1e19e3f40 100644
--- a/packages/api/src/api/graphql/titres.test.integration.ts
+++ b/packages/api/src/api/graphql/titres.test.integration.ts
@@ -1,7 +1,6 @@
 /* eslint-disable sql/no-unsafe-query */
 import { dbManager } from '../../../tests/db-manager'
 import { graphQLCall, queryImport } from '../../../tests/_utils/index'
-import options from '../../database/queries/_options'
 import { ADMINISTRATION_IDS } from 'camino-common/src/static/administrations'
 import { ITitre } from '../../types'
 import { newDemarcheId, newEtapeId, newTitreId } from '../../database/models/_format/id-create'
@@ -13,8 +12,8 @@ import { entrepriseUpsert } from '../../database/queries/entreprises'
 import { newEntrepriseId } from 'camino-common/src/entreprise'
 import { communeIdValidator } from 'camino-common/src/static/communes'
 import type { Knex } from 'knex'
-import Titres from '../../database/models/titres'
 import { ETAPE_IS_NOT_BROUILLON } from 'camino-common/src/etape'
+import { insertTitreGraph } from '../../../tests/integration-test-helper'
 
 console.info = vi.fn()
 console.error = vi.fn()
@@ -169,7 +168,7 @@ describe('titre', () => {
       publicLecture: true,
       propsTitreEtapesIds: {},
     }
-    await Titres.query().upsertGraph(titrePublicLecture, options.titres.update)
+    await insertTitreGraph(titrePublicLecture)
     const res = await graphQLCall(dbPool, titreQuery, { id: 'titre-id' }, undefined)
 
     expect(res.body.errors).toBe(undefined)
@@ -177,7 +176,7 @@ describe('titre', () => {
   })
 
   test('ne peut pas voir un titre qui n\'est pas en "lecture publique" (utilisateur anonyme)', async () => {
-    await Titres.query().upsertGraph(titrePublicLectureFalse, options.titres.update)
+    await insertTitreGraph(titrePublicLectureFalse)
     const res = await graphQLCall(dbPool, titreQuery, { id: 'titre-id' }, undefined)
 
     expect(res.body.errors).toBe(undefined)
@@ -185,7 +184,7 @@ describe('titre', () => {
   })
 
   test('ne peut voir que les démarches qui sont en "lecture publique" (utilisateur anonyme)', async () => {
-    await Titres.query().upsertGraph(titreDemarchesPubliques, options.titres.update)
+    await insertTitreGraph(titreDemarchesPubliques)
     const res = await graphQLCall(dbPool, titreQuery, { id: 'titre-id' }, undefined)
 
     expect(res.body.errors).toBe(undefined)
@@ -198,7 +197,7 @@ describe('titre', () => {
   })
 
   test('ne peut voir que les étapes qui sont en "lecture publique" (utilisateur anonyme)', async () => {
-    await Titres.query().upsertGraph(titreEtapesPubliques, options.titres.update)
+    await insertTitreGraph(titreEtapesPubliques)
     const res = await graphQLCall(dbPool, titreQuery, { id: 'titre-id' }, undefined)
 
     expect(res.body.errors).toBe(undefined)
@@ -215,7 +214,7 @@ describe('titre', () => {
   })
 
   test('ne peut pas voir certaines étapes (utilisateur DGTM)', async () => {
-    await Titres.query().upsertGraph(titreEtapesPubliques, options.titres.update)
+    await insertTitreGraph(titreEtapesPubliques)
     const res = await graphQLCall(dbPool, titreQuery, { id: 'titre-id' }, { role: 'admin', administrationId: ADMINISTRATION_IDS['DGTM - GUYANE'] })
 
     expect(res.body.errors).toBe(undefined)
@@ -239,7 +238,7 @@ describe('titre', () => {
   })
 
   test('ne peut pas voir certaines étapes (utilisateur ONF)', async () => {
-    await Titres.query().upsertGraph(titreEtapesPubliques, options.titres.update)
+    await insertTitreGraph(titreEtapesPubliques)
     const res = await graphQLCall(
       dbPool,
       titreQuery,
@@ -302,11 +301,10 @@ describe('titres', () => {
     await entrepriseUpsert({
       id: entrepriseId1,
       nom: `${entrepriseId1}`,
-      etablissements: [],
       archive: false,
     })
 
-    await Titres.query().upsertGraph(titre, options.titres.update)
+    await insertTitreGraph(titre)
 
     const res = await graphQLCall(
       dbPool,
@@ -337,11 +335,10 @@ describe('titres', () => {
     await entrepriseUpsert({
       id: entrepriseId1,
       nom: `${entrepriseId1}`,
-      etablissements: [],
       archive: false,
     })
 
-    await Titres.query().upsertGraph(titre, options.titres.update)
+    await insertTitreGraph(titre)
 
     const res = await graphQLCall(
       dbPool,
@@ -372,13 +369,12 @@ describe('titres', () => {
     await entrepriseUpsert({
       id: entrepriseId1,
       nom: `${entrepriseId1}`,
-      etablissements: [],
       archive: false,
     })
 
     const nomCommune = 'NOM DE COMMUNE'
 
-    await Titres.query().upsertGraph(titre, options.titres.update)
+    await insertTitreGraph(titre)
     await knexStuff.raw(`insert into communes (id, nom, geometry) values ('${communeId}', '${nomCommune}', '010100000000000000000000000000000000000000')`)
 
     const res = await graphQLCall(
diff --git a/packages/api/src/api/rest/demarches.queries.ts b/packages/api/src/api/rest/demarches.queries.ts
index aae046f66..e88684493 100644
--- a/packages/api/src/api/rest/demarches.queries.ts
+++ b/packages/api/src/api/rest/demarches.queries.ts
@@ -1,7 +1,7 @@
 import { sql } from '@pgtyped/runtime'
-import { DemarcheId, DemarcheIdOrSlug } from 'camino-common/src/demarche'
-import { Redefine, dbQueryAndValidate } from '../../pg-database'
-import { IGetDemarcheByIdOrSlugDbQuery, IGetEtapesByDemarcheIdDbQuery } from './demarches.queries.types'
+import { DemarcheId, DemarcheIdOrSlug, demarcheIdValidator } from 'camino-common/src/demarche'
+import { EffectDbQueryAndValidateErrors, Redefine, dbQueryAndValidate, effectDbQueryAndValidate } from '../../pg-database'
+import { IGetDemarcheByIdOrSlugDbQuery, IGetDemarchesDbQuery, IGetEtapesByDemarcheIdDbQuery } from './demarches.queries.types'
 import { z } from 'zod'
 import { caminoDateValidator } from 'camino-common/src/date'
 import { communeValidator } from 'camino-common/src/static/communes'
@@ -20,6 +20,8 @@ import { getDemarcheByIdOrSlugValidator as commonGetDemarcheByIdOrSlugValidator
 import { geoSystemeIdValidator } from 'camino-common/src/static/geoSystemes'
 import { entrepriseIdValidator } from 'camino-common/src/entreprise'
 import { km2Validator } from 'camino-common/src/number'
+import { Effect } from 'effect'
+import { CaminoError } from 'camino-common/src/zod-tools'
 
 const getEtapesByDemarcheIdDbValidator = z.object({
   id: etapeIdValidator,
@@ -51,11 +53,12 @@ const getEtapesByDemarcheIdDbValidator = z.object({
   titulaire_ids: z.array(entrepriseIdValidator),
   amodiataire_ids: z.array(entrepriseIdValidator),
   is_brouillon: etapeBrouillonValidator,
+  etape_fondamentale_id: etapeIdValidator,
 })
 
-export const getEtapesByDemarcheId = async (pool: Pool, demarcheId: DemarcheId) => {
-  return dbQueryAndValidate(getEtapesByDemarcheIdDb, { demarcheId }, pool, getEtapesByDemarcheIdDbValidator)
-}
+export const getEtapesByDemarcheId = (pool: Pool, demarcheId: DemarcheId): Effect.Effect<GetEtapesByDemarcheIdDb[], CaminoError<EffectDbQueryAndValidateErrors>> =>
+  effectDbQueryAndValidate(getEtapesByDemarcheIdDb, { demarcheId }, pool, getEtapesByDemarcheIdDbValidator)
+
 type GetEtapesByDemarcheIdDb = z.infer<typeof getEtapesByDemarcheIdDbValidator>
 const getEtapesByDemarcheIdDb = sql<Redefine<IGetEtapesByDemarcheIdDbQuery, { demarcheId: DemarcheId }, GetEtapesByDemarcheIdDb>>`
 select
@@ -87,7 +90,8 @@ select
     e.geojson_origine_forages,
     e.titulaire_ids,
     e.amodiataire_ids,
-    e.is_brouillon
+    e.is_brouillon,
+    e.etape_fondamentale_id
 from
     titres_etapes e
 where
@@ -127,3 +131,10 @@ where (td.id = $ idOrSlug !
     or td.slug = $ idOrSlug !)
 and td.archive is false
 `
+
+const getDemarchesValidator = z.object({ id: demarcheIdValidator })
+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>>`
+  select td.id from titres_demarches td where td.archive is false`
diff --git a/packages/api/src/api/rest/demarches.queries.types.ts b/packages/api/src/api/rest/demarches.queries.types.ts
index baa6be433..8ee27a1a2 100644
--- a/packages/api/src/api/rest/demarches.queries.types.ts
+++ b/packages/api/src/api/rest/demarches.queries.types.ts
@@ -15,6 +15,7 @@ export interface IGetEtapesByDemarcheIdDbResult {
   date_debut: string | null;
   date_fin: string | null;
   duree: number | null;
+  etape_fondamentale_id: string;
   etape_statut_id: string;
   etape_type_id: string;
   forets: Json;
@@ -70,3 +71,17 @@ export interface IGetDemarcheByIdOrSlugDbQuery {
   result: IGetDemarcheByIdOrSlugDbResult;
 }
 
+/** 'GetDemarchesDb' parameters type */
+export type IGetDemarchesDbParams = void;
+
+/** 'GetDemarchesDb' return type */
+export interface IGetDemarchesDbResult {
+  id: string;
+}
+
+/** 'GetDemarchesDb' query type */
+export interface IGetDemarchesDbQuery {
+  params: IGetDemarchesDbParams;
+  result: IGetDemarchesDbResult;
+}
+
diff --git a/packages/api/src/api/rest/demarches.test.integration.ts b/packages/api/src/api/rest/demarches.test.integration.ts
index 6a81a9823..4b3eb020c 100644
--- a/packages/api/src/api/rest/demarches.test.integration.ts
+++ b/packages/api/src/api/rest/demarches.test.integration.ts
@@ -6,9 +6,7 @@ import { HTTP_STATUS } from 'camino-common/src/http'
 import { toCaminoDate } from 'camino-common/src/date'
 import { titreSlugValidator } from 'camino-common/src/validators/titres'
 import { newTitreId, newDemarcheId, newEtapeId } from '../../database/models/_format/id-create'
-import { titreCreate } from '../../database/queries/titres'
 import TitresDemarches from '../../database/models/titres-demarches'
-import TitresEtapes from '../../database/models/titres-etapes'
 import Titres from '../../database/models/titres'
 import { userSuper } from '../../database/user-super'
 import { entrepriseIdValidator } from 'camino-common/src/entreprise'
@@ -19,6 +17,8 @@ import crypto from 'crypto'
 import { km2Validator } from 'camino-common/src/number'
 import { demarcheIdValidator } from 'camino-common/src/demarche'
 import { ADMINISTRATION_IDS } from 'camino-common/src/static/administrations'
+import { insertTitreGraph } from '../../../tests/integration-test-helper'
+import TitresEtapes from '../../database/models/titres-etapes'
 
 console.info = vi.fn()
 console.error = vi.fn()
@@ -109,6 +109,7 @@ describe('downloadDemarches', () => {
       geojsonOrigineGeoSystemeId: '4326',
       titulaireIds: [titulaireId],
       amodiataireIds: [amodiataireId],
+      etapeFondamentaleId: etapeId,
     })
   })
 
@@ -210,33 +211,31 @@ describe('demarcheSupprimer', () => {
 
 describe('demarcheCreer', () => {
   test('ne peut pas créer une démarche (utilisateur anonyme)', async () => {
-    const titre = await titreCreate(
-      {
-        nom: 'mon titre',
-        typeId: 'arm',
-        titreStatutId: 'ind',
-        propsTitreEtapesIds: {},
-        publicLecture: true,
-      },
-      {}
-    )
+    const titreId = newTitreId()
+    await insertTitreGraph({
+      id: titreId,
+      nom: 'mon titre',
+      typeId: 'arm',
+      titreStatutId: 'ind',
+      propsTitreEtapesIds: {},
+      publicLecture: true,
+    })
 
-    const res = await restNewPostCall(dbPool, '/rest/demarches', {}, undefined, { titreId: titre.id, typeId: 'oct', description: '' })
+    const res = await restNewPostCall(dbPool, '/rest/demarches', {}, undefined, { titreId, typeId: 'oct', description: '' })
 
     expect(res.status).toBe(HTTP_STATUS.FORBIDDEN)
   })
 
   test('ne peut pas créer une démarche (utilisateur editeur)', async () => {
-    const titre = await titreCreate(
-      {
-        nom: 'mon titre',
-        typeId: 'arm',
-        titreStatutId: 'ind',
-        propsTitreEtapesIds: {},
-        publicLecture: true,
-      },
-      {}
-    )
+    const titreId = newTitreId()
+    await insertTitreGraph({
+      id: titreId,
+      nom: 'mon titre',
+      typeId: 'arm',
+      titreStatutId: 'ind',
+      propsTitreEtapesIds: {},
+      publicLecture: true,
+    })
 
     const res = await restNewPostCall(
       dbPool,
@@ -246,7 +245,7 @@ describe('demarcheCreer', () => {
         role: 'editeur',
         administrationId: 'ope-onf-973-01',
       },
-      { titreId: titre.id, typeId: 'oct', description: '' }
+      { titreId, typeId: 'oct', description: '' }
     )
 
     expect(res.status).toBe(HTTP_STATUS.INTERNAL_SERVER_ERROR)
@@ -254,9 +253,8 @@ describe('demarcheCreer', () => {
   })
 
   test('peut créer une démarche (utilisateur super)', async () => {
-    const titre = await titreCreate({ nom: 'titre', typeId: 'arm', titreStatutId: 'val', propsTitreEtapesIds: {} }, {})
-
-    const titreId = titre.id
+    const titreId = newTitreId()
+    await insertTitreGraph({ id: titreId, nom: 'titre', typeId: 'arm', titreStatutId: 'val', propsTitreEtapesIds: {} })
 
     const res = await restNewPostCall(dbPool, '/rest/demarches', {}, userSuper, { titreId, typeId: 'oct', description: '' })
 
@@ -280,9 +278,8 @@ describe('demarcheCreer', () => {
   })
 
   test('peut créer une démarche (utilisateur admin)', async () => {
-    const titre = await titreCreate({ nom: 'titre', typeId: 'arm', titreStatutId: 'val', propsTitreEtapesIds: {} }, {})
-
-    const titreId = titre.id
+    const titreId = newTitreId()
+    await insertTitreGraph({ id: titreId, nom: 'titre', typeId: 'arm', titreStatutId: 'val', propsTitreEtapesIds: {} })
 
     const res = await restNewPostCall(
       dbPool,
@@ -300,15 +297,14 @@ describe('demarcheCreer', () => {
   })
 
   test("ne peut pas créer une démarche sur un titre ARM échu (un utilisateur 'admin' PTMG)", async () => {
-    const titre = await titreCreate(
-      {
-        nom: 'mon titre échu',
-        typeId: 'arm',
-        titreStatutId: 'ech',
-        propsTitreEtapesIds: {},
-      },
-      {}
-    )
+    const titreId = newTitreId()
+    await insertTitreGraph({
+      id: titreId,
+      nom: 'mon titre échu',
+      typeId: 'arm',
+      titreStatutId: 'ech',
+      propsTitreEtapesIds: {},
+    })
     const res = await restNewPostCall(
       dbPool,
       '/rest/demarches',
@@ -317,7 +313,7 @@ describe('demarcheCreer', () => {
         role: 'admin',
         administrationId: ADMINISTRATION_IDS['PÔLE TECHNIQUE MINIER DE GUYANE'],
       },
-      { titreId: titre.id, typeId: 'oct', description: '' }
+      { titreId, typeId: 'oct', description: '' }
     )
 
     expect(res.status).toBe(HTTP_STATUS.INTERNAL_SERVER_ERROR)
@@ -326,20 +322,19 @@ describe('demarcheCreer', () => {
 })
 
 const demarcheCreate = async () => {
-  const titre = await titreCreate(
-    {
-      nom: 'mon titre',
-      typeId: 'arm',
-      titreStatutId: 'ind',
-      propsTitreEtapesIds: {},
-    },
-    {}
-  )
+  const titreId = newTitreId()
+  await insertTitreGraph({
+    id: titreId,
+    nom: 'mon titre',
+    typeId: 'arm',
+    titreStatutId: 'ind',
+    propsTitreEtapesIds: {},
+  })
 
-  const titreDemarche = await TitresDemarches.query().insertAndFetch({ titreId: titre.id, typeId: 'oct' })
+  const titreDemarche = await TitresDemarches.query().insertAndFetch({ titreId, typeId: 'oct' })
 
   return {
-    titreId: titre.id,
+    titreId,
     demarcheId: titreDemarche.id,
   }
 }
diff --git a/packages/api/src/api/rest/entreprises.test.integration.ts b/packages/api/src/api/rest/entreprises.test.integration.ts
index 913f2e7aa..516364007 100644
--- a/packages/api/src/api/rest/entreprises.test.integration.ts
+++ b/packages/api/src/api/rest/entreprises.test.integration.ts
@@ -10,18 +10,18 @@ import { tempDocumentNameValidator } from 'camino-common/src/document'
 import { entreprisesEtablissementsFetch, entreprisesFetch, tokenInitialize } from '../../tools/api-insee/fetch'
 import { entreprise, entrepriseAndEtablissements } from '../../../tests/__mocks__/fetch-insee-api'
 import type { Pool } from 'pg'
-import { titreCreate } from '../../database/queries/titres'
 import { titreDemarcheCreate } from '../../database/queries/titres-demarches'
 import { titreEtapeCreate } from '../../database/queries/titres-etapes'
 import { toCaminoAnnee, toCaminoDate } from 'camino-common/src/date'
 import { HTTP_STATUS } from 'camino-common/src/http'
 import { copyFileSync, mkdirSync } from 'node:fs'
-import { idGenerate } from '../../database/models/_format/id-create'
+import { idGenerate, newTitreId } from '../../database/models/_format/id-create'
 import { insertTitreEtapeEntrepriseDocument } from '../../database/queries/titres-etapes.queries'
 import { titreSlugValidator } from 'camino-common/src/validators/titres'
 import type { Knex } from 'knex'
 import { ETAPE_IS_NOT_BROUILLON } from 'camino-common/src/etape'
 import { etapeCreate } from './rest-test-utils'
+import { insertTitreGraph } from '../../../tests/integration-test-helper'
 console.info = vi.fn()
 console.warn = vi.fn()
 console.error = vi.fn()
@@ -308,19 +308,18 @@ describe('getEntrepriseDocument', () => {
     const entrepriseId = newEntrepriseId('get-entreprise-document-entreprise-id')
     await entrepriseUpsert({ id: entrepriseId, nom: entrepriseId })
 
-    const titre = await titreCreate(
-      {
-        nom: '',
-        typeId: 'arm',
-        titreStatutId: 'ind',
-        slug: titreSlugValidator.parse('arm-slug'),
-        propsTitreEtapesIds: {},
-      },
-      {}
-    )
+    const titreId = newTitreId()
+    await insertTitreGraph({
+      id: titreId,
+      nom: '',
+      typeId: 'arm',
+      titreStatutId: 'ind',
+      slug: titreSlugValidator.parse('arm-slug'),
+      propsTitreEtapesIds: {},
+    })
 
     const titreDemarche = await titreDemarcheCreate({
-      titreId: titre.id,
+      titreId,
       typeId: 'oct',
     })
     const titreEtape = await titreEtapeCreate(
@@ -333,7 +332,7 @@ describe('getEntrepriseDocument', () => {
         isBrouillon: ETAPE_IS_NOT_BROUILLON,
       },
       userSuper,
-      titre.id
+      titreId
     )
 
     const fileName = `existing_temp_file_${idGenerate()}`
diff --git a/packages/api/src/api/rest/etape-creer.test.integration.ts b/packages/api/src/api/rest/etape-creer.test.integration.ts
index 123c61355..536b37299 100644
--- a/packages/api/src/api/rest/etape-creer.test.integration.ts
+++ b/packages/api/src/api/rest/etape-creer.test.integration.ts
@@ -1,7 +1,5 @@
 import { dbManager } from '../../../tests/db-manager'
 import { restPostCall } from '../../../tests/_utils/index'
-import { titreDemarcheCreate } from '../../database/queries/titres-demarches'
-import { titreCreate } from '../../database/queries/titres'
 import Titres from '../../database/models/titres'
 import { ADMINISTRATION_IDS } from 'camino-common/src/static/administrations'
 import { userSuper } from '../../database/user-super'
@@ -14,6 +12,8 @@ import { HTTP_STATUS } from 'camino-common/src/http'
 import { toCaminoDate } from 'camino-common/src/date'
 import { entrepriseIdValidator } from 'camino-common/src/entreprise'
 import { TitreTypeId } from 'camino-common/src/static/titresTypes'
+import { newDemarcheId, newTitreId } from '../../database/models/_format/id-create'
+import { insertTitreGraph } from '../../../tests/integration-test-helper'
 
 console.info = vi.fn()
 console.error = vi.fn()
@@ -33,22 +33,24 @@ afterAll(async () => {
 })
 
 const demarcheCreate = async (titreTypeId: TitreTypeId = 'arm') => {
-  const titre = await titreCreate(
-    {
-      nom: 'mon titre',
-      typeId: titreTypeId,
-      titreStatutId: 'ind',
-      propsTitreEtapesIds: {},
-    },
-    {}
-  )
-
-  const titreDemarche = await titreDemarcheCreate({
-    titreId: titre.id,
-    typeId: 'oct',
+  const titreId = newTitreId()
+  const titreDemarcheId = newDemarcheId()
+  await insertTitreGraph({
+    id: titreId,
+    nom: 'mon titre',
+    typeId: titreTypeId,
+    titreStatutId: 'ind',
+    propsTitreEtapesIds: {},
+    demarches: [
+      {
+        id: titreDemarcheId,
+        titreId: titreId,
+        typeId: 'oct',
+      },
+    ],
   })
 
-  return titreDemarche.id
+  return titreDemarcheId
 }
 
 const blankEtapeProps: Pick<
diff --git a/packages/api/src/api/rest/etapes.test.integration.ts b/packages/api/src/api/rest/etapes.test.integration.ts
index 0fe8fd23d..bb5bcb891 100644
--- a/packages/api/src/api/rest/etapes.test.integration.ts
+++ b/packages/api/src/api/rest/etapes.test.integration.ts
@@ -1,6 +1,4 @@
 import { dbManager } from '../../../tests/db-manager'
-import { titreCreate } from '../../database/queries/titres'
-import { titreDemarcheCreate } from '../../database/queries/titres-demarches'
 import { userSuper } from '../../database/user-super'
 import { restCall, restDeleteCall, restNewCall } from '../../../tests/_utils/index'
 import { caminoDateValidator, toCaminoDate } from 'camino-common/src/date'
@@ -8,7 +6,7 @@ import { afterAll, beforeAll, test, expect, describe, vi } from 'vitest'
 import type { Pool } from 'pg'
 import { HTTP_STATUS } from 'camino-common/src/http'
 import { Role, isAdministrationRole } from 'camino-common/src/roles'
-import { titreEtapeCreate, titreEtapeUpdate } from '../../database/queries/titres-etapes'
+import { titreEtapeUpdate } from '../../database/queries/titres-etapes'
 import { entrepriseIdValidator } from 'camino-common/src/entreprise'
 import { TestUser, testBlankUser } from 'camino-common/src/tests-utils'
 import { entrepriseUpsert } from '../../database/queries/entreprises'
@@ -18,6 +16,8 @@ import { insertEtapeAvisWithLargeObjectId } from '../../database/queries/titres-
 import { largeObjectIdValidator } from '../../database/largeobjects'
 import { AvisVisibilityIds } from 'camino-common/src/static/avisTypes'
 import { tempDocumentNameValidator } from 'camino-common/src/document'
+import { newDemarcheId, newEtapeId, newTitreId } from '../../database/models/_format/id-create'
+import { insertTitreGraph } from '../../../tests/integration-test-helper'
 
 console.info = vi.fn()
 console.error = vi.fn()
@@ -36,22 +36,24 @@ afterAll(async () => {
 
 describe('getEtapesTypesEtapesStatusWithMainStep', () => {
   test('nouvelle étapes possibles', async () => {
-    const titre = await titreCreate(
-      {
-        nom: 'nomTitre',
-        typeId: 'arm',
-        titreStatutId: 'val',
-        propsTitreEtapesIds: {},
-      },
-      {}
-    )
-
-    const titreDemarche = await titreDemarcheCreate({
-      titreId: titre.id,
-      typeId: 'oct',
+    const titreId = newTitreId()
+    const demarcheId = newDemarcheId()
+    await insertTitreGraph({
+      id: titreId,
+      nom: 'nomTitre',
+      typeId: 'arm',
+      titreStatutId: 'val',
+      propsTitreEtapesIds: {},
+      demarches: [
+        {
+          id: demarcheId,
+          titreId,
+          typeId: 'oct',
+        },
+      ],
     })
 
-    const tested = await restNewCall(dbPool, '/rest/etapesTypes/:demarcheId/:date', { demarcheId: titreDemarche.id, date: toCaminoDate('2024-09-01') }, userSuper)
+    const tested = await restNewCall(dbPool, '/rest/etapesTypes/:demarcheId/:date', { demarcheId: demarcheId, date: toCaminoDate('2024-09-01') }, userSuper)
 
     expect(tested.statusCode).toBe(HTTP_STATUS.OK)
     expect(tested.body).toMatchInlineSnapshot(`
@@ -86,34 +88,35 @@ describe('getEtapesTypesEtapesStatusWithMainStep', () => {
     `)
   })
   test('nouvelle étapes possibles prends en compte les brouillons', async () => {
-    const titre = await titreCreate(
-      {
-        nom: 'nomTitre',
-        typeId: 'arm',
-        titreStatutId: 'val',
-        propsTitreEtapesIds: {},
-      },
-      {}
-    )
-
-    const titreDemarche = await titreDemarcheCreate({
-      titreId: titre.id,
-      typeId: 'oct',
+    const titreId = newTitreId()
+    const demarcheId = newDemarcheId()
+    const etapeId = newEtapeId()
+    await insertTitreGraph({
+      id: titreId,
+      nom: 'nomTitre',
+      typeId: 'arm',
+      titreStatutId: 'val',
+      propsTitreEtapesIds: {},
+      demarches: [
+        {
+          id: demarcheId,
+          titreId,
+          typeId: 'oct',
+          etapes: [
+            {
+              id: etapeId,
+              typeId: 'mfr',
+              date: toCaminoDate('2024-06-27'),
+              titreDemarcheId: demarcheId,
+              statutId: 'fai',
+              isBrouillon: ETAPE_IS_BROUILLON,
+            },
+          ],
+        },
+      ],
     })
 
-    await titreEtapeCreate(
-      {
-        typeId: 'mfr',
-        date: toCaminoDate('2024-06-27'),
-        titreDemarcheId: titreDemarche.id,
-        statutId: 'fai',
-        isBrouillon: ETAPE_IS_BROUILLON,
-      },
-      userSuper,
-      titre.id
-    )
-
-    const tested = await restNewCall(dbPool, '/rest/etapesTypes/:demarcheId/:date', { demarcheId: titreDemarche.id, date: toCaminoDate('2024-09-01') }, userSuper)
+    const tested = await restNewCall(dbPool, '/rest/etapesTypes/:demarcheId/:date', { demarcheId: demarcheId, date: toCaminoDate('2024-09-01') }, userSuper)
 
     expect(tested.statusCode).toBe(HTTP_STATUS.OK)
     expect(tested.body).toMatchInlineSnapshot(`
@@ -145,36 +148,39 @@ describe('getEtapesTypesEtapesStatusWithMainStep', () => {
 
 describe('etapeSupprimer', () => {
   test.each([undefined, 'admin' as Role])('ne peut pas supprimer une étape (utilisateur %s)', async (role: Role | undefined) => {
-    const titre = await titreCreate(
-      {
-        nom: 'mon titre',
-        typeId: 'arm',
-        titreStatutId: 'ind',
-        propsTitreEtapesIds: {},
-      },
-      {}
-    )
-    const titreDemarche = await titreDemarcheCreate({
-      titreId: titre.id,
-      typeId: 'oct',
+    const titreId = newTitreId()
+    const demarcheId = newDemarcheId()
+    const etapeId = newEtapeId()
+    await insertTitreGraph({
+      id: titreId,
+      nom: 'nomTitre',
+      typeId: 'arm',
+      titreStatutId: 'ind',
+      propsTitreEtapesIds: {},
+      demarches: [
+        {
+          id: demarcheId,
+          titreId,
+          typeId: 'oct',
+          etapes: [
+            {
+              id: etapeId,
+              typeId: 'mfr',
+              statutId: 'fai',
+              isBrouillon: ETAPE_IS_BROUILLON,
+              ordre: 1,
+              titreDemarcheId: demarcheId,
+              date: toCaminoDate('2018-01-01'),
+            },
+          ],
+        },
+      ],
     })
 
-    const titreEtape = await titreEtapeCreate(
-      {
-        typeId: 'mfr',
-        statutId: 'fai',
-        isBrouillon: ETAPE_IS_BROUILLON,
-        ordre: 1,
-        titreDemarcheId: titreDemarche.id,
-        date: toCaminoDate('2018-01-01'),
-      },
-      userSuper,
-      titre.id
-    )
     const tested = await restDeleteCall(
       dbPool,
       '/rest/etapes/:etapeIdOrSlug',
-      { etapeIdOrSlug: titreEtape.id },
+      { etapeIdOrSlug: etapeId },
       role && isAdministrationRole(role) ? { role, administrationId: 'min-mctrct-dgcl-01' } : undefined
     )
 
@@ -182,85 +188,92 @@ describe('etapeSupprimer', () => {
   })
 
   test('peut supprimer une étape (utilisateur super)', async () => {
-    const titre = await titreCreate(
-      {
-        nom: 'mon titre',
-        typeId: 'arm',
-        titreStatutId: 'ind',
-        propsTitreEtapesIds: {},
-      },
-      {}
-    )
-    const titreDemarche = await titreDemarcheCreate({
-      titreId: titre.id,
-      typeId: 'oct',
+    const titreId = newTitreId()
+    const demarcheId = newDemarcheId()
+    const etapeId = newEtapeId()
+    await insertTitreGraph({
+      id: titreId,
+      nom: 'nomTitre',
+      typeId: 'arm',
+      titreStatutId: 'ind',
+      propsTitreEtapesIds: {},
+      demarches: [
+        {
+          id: demarcheId,
+          titreId,
+          typeId: 'oct',
+          etapes: [
+            {
+              id: etapeId,
+              typeId: 'mfr',
+              statutId: 'fai',
+              isBrouillon: ETAPE_IS_BROUILLON,
+              ordre: 1,
+              titreDemarcheId: demarcheId,
+              date: toCaminoDate('2018-01-01'),
+            },
+          ],
+        },
+      ],
     })
 
-    const titreEtape = await titreEtapeCreate(
-      {
-        typeId: 'mfr',
-        statutId: 'fai',
-        isBrouillon: ETAPE_IS_BROUILLON,
-        ordre: 1,
-        titreDemarcheId: titreDemarche.id,
-        date: toCaminoDate('2018-01-01'),
-      },
-      userSuper,
-      titre.id
-    )
-    const tested = await restDeleteCall(dbPool, '/rest/etapes/:etapeIdOrSlug', { etapeIdOrSlug: titreEtape.id }, userSuper)
+    const tested = await restDeleteCall(dbPool, '/rest/etapes/:etapeIdOrSlug', { etapeIdOrSlug: etapeId }, userSuper)
 
     expect(tested.statusCode).toBe(HTTP_STATUS.NO_CONTENT)
   })
 
   test('un titulaire peut voir mais ne peut pas supprimer sa demande', async () => {
-    const titre = await titreCreate(
-      {
-        nom: 'mon titre',
-        typeId: 'arm',
-        titreStatutId: 'ind',
-        propsTitreEtapesIds: {},
-        publicLecture: false,
-      },
-      {}
-    )
-    const titreDemarche = await titreDemarcheCreate({
-      titreId: titre.id,
-      typeId: 'oct',
-      publicLecture: false,
-      entreprisesLecture: true,
-    })
     const titulaireId1 = entrepriseIdValidator.parse('titulaireid1')
     await entrepriseUpsert({
       id: titulaireId1,
       nom: 'Mon Entreprise',
     })
-    const titreEtape = await titreEtapeCreate(
-      {
-        typeId: 'mfr',
-        statutId: 'fai',
-        isBrouillon: ETAPE_IS_BROUILLON,
-        ordre: 1,
-        titreDemarcheId: titreDemarche.id,
-        date: toCaminoDate('2018-01-01'),
-        titulaireIds: [titulaireId1],
-      },
-      userSuper,
-      titre.id
-    )
+    const titreId = newTitreId()
+    const demarcheId = newDemarcheId()
+    const etapeId = newEtapeId()
+    await insertTitreGraph({
+      id: titreId,
+      nom: 'nomTitre',
+      typeId: 'arm',
+      titreStatutId: 'val',
+      propsTitreEtapesIds: {},
+      publicLecture: false,
+      demarches: [
+        {
+          id: demarcheId,
+          titreId,
+          typeId: 'oct',
+          publicLecture: false,
+          entreprisesLecture: true,
+          etapes: [
+            {
+              id: etapeId,
+              typeId: 'mfr',
+              statutId: 'fai',
+              isBrouillon: ETAPE_IS_BROUILLON,
+              ordre: 1,
+              titreDemarcheId: demarcheId,
+              date: toCaminoDate('2018-01-01'),
+              titulaireIds: [titulaireId1],
+            },
+          ],
+        },
+      ],
+    })
+
     await knex('titres')
-      .update({ propsTitreEtapesIds: { titulaires: titreEtape.id } })
-      .where('id', titre.id)
+      .update({ propsTitreEtapesIds: { titulaires: etapeId } })
+      .where('id', titreId)
     const user: TestUser = {
       ...testBlankUser,
       role: 'entreprise',
       entrepriseIds: [titulaireId1],
     }
 
-    const getEtape = await restCall(dbPool, '/rest/titres/:titreId', { titreId: titre.id }, user)
+    const getEtape = await restCall(dbPool, '/rest/titres/:titreId', { titreId: titreId }, user)
     expect(getEtape.statusCode).toBe(HTTP_STATUS.OK)
 
-    const tested = await restDeleteCall(dbPool, '/rest/etapes/:etapeIdOrSlug', { etapeIdOrSlug: titreEtape.id }, user)
+    const tested = await restDeleteCall(dbPool, '/rest/etapes/:etapeIdOrSlug', { etapeIdOrSlug: etapeId }, user)
 
     expect(tested.statusCode).toBe(HTTP_STATUS.FORBIDDEN)
   })
@@ -268,46 +281,47 @@ describe('etapeSupprimer', () => {
 
 describe('getEtapeAvis', () => {
   test('test la récupération des avis', async () => {
-    const titre = await titreCreate(
-      {
-        nom: 'nomTitre',
-        typeId: 'arm',
-        titreStatutId: 'val',
-        propsTitreEtapesIds: {},
-      },
-      {}
-    )
-
-    const titreDemarche = await titreDemarcheCreate({
-      titreId: titre.id,
-      typeId: 'oct',
+    const titreId = newTitreId()
+    const demarcheId = newDemarcheId()
+    const etapeId = newEtapeId()
+    await insertTitreGraph({
+      id: titreId,
+      nom: 'nomTitre',
+      typeId: 'arm',
+      titreStatutId: 'val',
+      propsTitreEtapesIds: {},
+      demarches: [
+        {
+          id: demarcheId,
+          titreId,
+          typeId: 'oct',
+          etapes: [
+            {
+              id: etapeId,
+              typeId: 'mfr',
+              statutId: 'fai',
+              isBrouillon: ETAPE_IS_BROUILLON,
+              ordre: 1,
+              titreDemarcheId: demarcheId,
+              date: toCaminoDate('2018-01-01'),
+            },
+          ],
+        },
+      ],
     })
 
-    const titreEtape = await titreEtapeCreate(
-      {
-        typeId: 'mfr',
-        statutId: 'fai',
-        isBrouillon: ETAPE_IS_BROUILLON,
-        ordre: 1,
-        titreDemarcheId: titreDemarche.id,
-        date: toCaminoDate('2018-01-01'),
-      },
-      userSuper,
-      titre.id
-    )
-
-    let getAvis = await restCall(dbPool, '/rest/etapes/:etapeId/etapeAvis', { etapeId: titreEtape.id }, userSuper)
+    let getAvis = await restCall(dbPool, '/rest/etapes/:etapeId/etapeAvis', { etapeId: etapeId }, userSuper)
     expect(getAvis.statusCode).toBe(HTTP_STATUS.OK)
     expect(getAvis.body).toStrictEqual([])
 
-    await titreEtapeUpdate(titreEtape.id, { typeId: 'asc' }, userSuper, titre.id)
-    getAvis = await restCall(dbPool, '/rest/etapes/:etapeId/etapeAvis', { etapeId: titreEtape.id }, userSuper)
+    await titreEtapeUpdate(etapeId, { typeId: 'asc' }, userSuper, titreId)
+    getAvis = await restCall(dbPool, '/rest/etapes/:etapeId/etapeAvis', { etapeId: etapeId }, userSuper)
     expect(getAvis.statusCode).toBe(HTTP_STATUS.OK)
     expect(getAvis.body).toStrictEqual([])
 
     await insertEtapeAvisWithLargeObjectId(
       dbPool,
-      titreEtape.id,
+      etapeId,
       {
         avis_type_id: 'autreAvis',
         date: caminoDateValidator.parse('2023-02-01'),
@@ -319,7 +333,7 @@ describe('getEtapeAvis', () => {
       etapeAvisIdValidator.parse('avisId'),
       largeObjectIdValidator.parse(42)
     )
-    getAvis = await restCall(dbPool, '/rest/etapes/:etapeId/etapeAvis', { etapeId: titreEtape.id }, userSuper)
+    getAvis = await restCall(dbPool, '/rest/etapes/:etapeId/etapeAvis', { etapeId: etapeId }, userSuper)
     expect(getAvis.statusCode).toBe(HTTP_STATUS.OK)
     expect(getAvis.body).toMatchInlineSnapshot(`
       [
diff --git a/packages/api/src/api/rest/journal.test.integration.ts b/packages/api/src/api/rest/journal.test.integration.ts
index e10784d3b..077735342 100644
--- a/packages/api/src/api/rest/journal.test.integration.ts
+++ b/packages/api/src/api/rest/journal.test.integration.ts
@@ -1,13 +1,13 @@
 /* eslint-disable sql/no-unsafe-query */
 import { dbManager } from '../../../tests/db-manager'
-import { titreCreate } from '../../database/queries/titres'
 import { afterAll, beforeAll, test, expect, vi, describe } from 'vitest'
 import type { Pool } from 'pg'
 import { getTitresModifiesByMonth } from './journal.queries'
 import { Knex } from 'knex'
-import { idGenerate } from '../../database/models/_format/id-create'
+import { idGenerate, newTitreId } from '../../database/models/_format/id-create'
 import { userGenerate } from '../../../tests/_utils/index'
-import { ITitre } from '../../types'
+import { insertTitreGraph } from '../../../tests/integration-test-helper'
+import { TitreId } from 'camino-common/src/validators/titres'
 
 console.info = vi.fn()
 console.error = vi.fn()
@@ -15,21 +15,20 @@ console.error = vi.fn()
 let dbPool: Pool
 let knex: Knex
 
-let titre: ITitre
+let titreId: TitreId
 beforeAll(async () => {
   const { pool, knex: knexInstance } = await dbManager.populateDb()
   dbPool = pool
   knex = knexInstance
 
-  titre = await titreCreate(
-    {
-      nom: 'nomTitre',
-      typeId: 'arm',
-      titreStatutId: 'val',
-      propsTitreEtapesIds: {},
-    },
-    {}
-  )
+  titreId = newTitreId()
+  await insertTitreGraph({
+    id: titreId,
+    nom: 'nomTitre',
+    typeId: 'arm',
+    titreStatutId: 'val',
+    propsTitreEtapesIds: {},
+  })
 })
 
 afterAll(async () => {
@@ -42,7 +41,7 @@ describe('getTitresModifiesByMonth', async () => {
 
     await knex.raw(
       `INSERT INTO public.journaux (id, utilisateur_id, date, element_id, operation, titre_id) VALUES ('${idGenerate()}', 'super', '2021-11-10 09:02:19.012000 +00:00', '${idGenerate()}', 'update', '${
-        titre.id
+        titreId
       }')`
     )
     tested = await getTitresModifiesByMonth(dbPool)
@@ -56,7 +55,7 @@ describe('getTitresModifiesByMonth', async () => {
     await knex.raw(
       `INSERT INTO public.journaux (id, utilisateur_id, date, element_id, operation, titre_id) VALUES ('${idGenerate()}', '${
         user.id
-      }', '2021-11-10 09:02:19.012000 +00:00', '${idGenerate()}', 'update', '${titre.id}')`
+      }', '2021-11-10 09:02:19.012000 +00:00', '${idGenerate()}', 'update', '${titreId}')`
     )
     tested = await getTitresModifiesByMonth(dbPool)
     expect(tested).toMatchInlineSnapshot(`
@@ -71,7 +70,7 @@ describe('getTitresModifiesByMonth', async () => {
     await knex.raw(
       `INSERT INTO public.journaux (id, utilisateur_id, date, element_id, operation, titre_id) VALUES ('${idGenerate()}', '${
         user.id
-      }', '2021-11-10 09:02:19.012000 +00:00', '${idGenerate()}', 'update', '${titre.id}')`
+      }', '2021-11-10 09:02:19.012000 +00:00', '${idGenerate()}', 'update', '${titreId}')`
     )
     tested = await getTitresModifiesByMonth(dbPool)
     expect(tested).toMatchInlineSnapshot(`
diff --git a/packages/api/src/api/rest/perimetre.ts b/packages/api/src/api/rest/perimetre.ts
index cf53446b9..4f91ea0f4 100644
--- a/packages/api/src/api/rest/perimetre.ts
+++ b/packages/api/src/api/rest/perimetre.ts
@@ -77,7 +77,7 @@ export const getPerimetreInfos =
             etape = { demarche_id: myEtape.demarche_id, geojson4326_perimetre: myEtape.geojson4326_perimetre, sdom_zones: myEtape.sdom_zones ?? [], etape_type_id: myEtape.etape_type_id, communes: [] }
           } else if (demarcheIdOrSlugParsed.success) {
             const demarche = await getDemarcheByIdOrSlug(pool, demarcheIdOrSlugParsed.data)
-            const etapes = await getEtapesByDemarcheId(pool, demarche.demarche_id)
+            const etapes = await callAndExit(getEtapesByDemarcheId(pool, demarche.demarche_id), async value => value)
 
             const mostRecentEtapeFondamentale = getMostRecentEtapeFondamentaleValide([{ ordre: 1, etapes }])
             if (isNotNullNorUndefined(mostRecentEtapeFondamentale)) {
diff --git a/packages/api/src/api/rest/rest-test-utils.ts b/packages/api/src/api/rest/rest-test-utils.ts
index 5ebac4c5a..f6a953435 100644
--- a/packages/api/src/api/rest/rest-test-utils.ts
+++ b/packages/api/src/api/rest/rest-test-utils.ts
@@ -4,11 +4,9 @@ import { EtapeId } from 'camino-common/src/etape'
 import { EtapeTypeId, canBeBrouillon } from 'camino-common/src/static/etapesTypes'
 import { TitreTypeId } from 'camino-common/src/static/titresTypes'
 import { isNotNullNorUndefined } from 'camino-common/src/typescript-tools'
-import { titreCreate } from '../../database/queries/titres'
-import { titreDemarcheCreate } from '../../database/queries/titres-demarches'
-import { titreEtapeCreate } from '../../database/queries/titres-etapes'
-import { userSuper } from '../../database/user-super'
 import { TitreId } from 'camino-common/src/validators/titres'
+import { insertTitreGraph } from '../../../tests/integration-test-helper'
+import { newDemarcheId, newEtapeId, newTitreId } from '../../database/models/_format/id-create'
 
 export async function etapeCreate(
   typeId?: EtapeTypeId,
@@ -19,33 +17,36 @@ export async function etapeCreate(
   titreEtapeId: EtapeId
   titreId: TitreId
 }> {
-  const titre = await titreCreate(
-    {
-      nom: 'mon titre',
-      typeId: titreTypeId,
-      titreStatutId: 'ind',
-      propsTitreEtapesIds: {},
-    },
-    {}
-  )
-  const titreDemarche = await titreDemarcheCreate({
-    titreId: titre.id,
-    typeId: 'oct',
-  })
-
+  const titreId = newTitreId()
+  const demarcheId = newDemarcheId()
+  const etapeId = newEtapeId()
   const myTypeId = isNotNullNorUndefined(typeId) ? typeId : 'mfr'
-  const titreEtape = await titreEtapeCreate(
-    {
-      typeId: myTypeId,
-      statutId: 'fai',
-      ordre: 1,
-      titreDemarcheId: titreDemarche.id,
-      date,
-      isBrouillon: canBeBrouillon(myTypeId),
-    },
-    userSuper,
-    titre.id
-  )
+  await insertTitreGraph({
+    id: titreId,
+    nom: 'mon titre',
+    typeId: titreTypeId,
+    titreStatutId: 'ind',
+    propsTitreEtapesIds: {},
+    demarches: [
+      {
+        id: demarcheId,
+        titreId: titreId,
+        typeId: 'oct',
+
+        etapes: [
+          {
+            id: etapeId,
+            typeId: myTypeId,
+            statutId: 'fai',
+            ordre: 1,
+            titreDemarcheId: demarcheId,
+            date,
+            isBrouillon: canBeBrouillon(myTypeId),
+          },
+        ],
+      },
+    ],
+  })
 
-  return { titreId: titre.id, titreDemarcheId: titreDemarche.id, titreEtapeId: titreEtape.id }
+  return { titreId, titreDemarcheId: demarcheId, titreEtapeId: etapeId }
 }
diff --git a/packages/api/src/api/rest/titres.queries.ts b/packages/api/src/api/rest/titres.queries.ts
index fbf075173..f9edecc2b 100644
--- a/packages/api/src/api/rest/titres.queries.ts
+++ b/packages/api/src/api/rest/titres.queries.ts
@@ -41,6 +41,7 @@ import { EntrepriseId, entrepriseIdValidator } from 'camino-common/src/entrepris
 import { AdministrationId } from 'camino-common/src/static/administrations'
 import { secteurMaritimeValidator } from 'camino-common/src/static/facades'
 import { getAvisTypes } from 'camino-common/src/permissions/etape-form'
+import { callAndExit } from '../../tools/fp-tools'
 
 type SuperEtapeDemarcheTitreGet = OmitDistributive<DemarcheEtape, 'etape_documents' | 'avis_documents'>
 type SuperDemarcheTitreGet = Omit<TitreGet['demarches'][0], 'etapes'> & { etapes: SuperEtapeDemarcheTitreGet[]; public_lecture: boolean; entreprises_lecture: boolean; titre_public_lecture: boolean }
@@ -61,7 +62,7 @@ export const getTitre = async (pool: Pool, user: User, idOrSlug: TitreIdOrSlug):
 
     const superDemarches: SuperDemarcheTitreGet[] = []
     for (const demarche of demarchesFromDatabase) {
-      const etapes = await getEtapesByDemarcheId(pool, demarche.id)
+      const etapes = await callAndExit(getEtapesByDemarcheId(pool, demarche.id), async value => value)
 
       const formatedEtapes: SuperEtapeDemarcheTitreGet[] = []
       for (const etape of etapes) {
@@ -338,7 +339,7 @@ const getDemarchesByTitreIdQueryDbValidator = z.object({
 })
 type GetDemarchesByTitreIdQueryDb = z.infer<typeof getDemarchesByTitreIdQueryDbValidator>
 
-export const getDemarchesByTitreId = async (pool: Pool, titreId: TitreId) => {
+export const getDemarchesByTitreId = async (pool: Pool, titreId: TitreId): Promise<GetDemarchesByTitreIdQueryDb[]> => {
   return dbQueryAndValidate(getDemarchesByTitreIdQueryDb, { titreId }, pool, getDemarchesByTitreIdQueryDbValidator)
 }
 const getDemarchesByTitreIdQueryDb = sql<Redefine<IGetDemarchesByTitreIdQueryDbQuery, { titreId: TitreId }, GetDemarchesByTitreIdQueryDb>>`
diff --git a/packages/api/src/api/rest/titres.test.integration.ts b/packages/api/src/api/rest/titres.test.integration.ts
index 46a44175f..5fd49f3c4 100644
--- a/packages/api/src/api/rest/titres.test.integration.ts
+++ b/packages/api/src/api/rest/titres.test.integration.ts
@@ -1,5 +1,5 @@
 import { dbManager } from '../../../tests/db-manager'
-import { titreCreate, titreUpdate } from '../../database/queries/titres'
+import { titreUpdate } from '../../database/queries/titres'
 import { titreDemarcheCreate } from '../../database/queries/titres-demarches'
 import { titreEtapeCreate } from '../../database/queries/titres-etapes'
 import { userSuper } from '../../database/user-super'
@@ -21,6 +21,7 @@ import TitresDemarches from '../../database/models/titres-demarches'
 import TitresEtapes from '../../database/models/titres-etapes'
 import Titres from '../../database/models/titres'
 import { ETAPE_IS_NOT_BROUILLON } from 'camino-common/src/etape'
+import { insertTitreGraph } from '../../../tests/integration-test-helper'
 
 console.info = vi.fn()
 console.error = vi.fn()
@@ -34,15 +35,13 @@ beforeAll(async () => {
 
   await insertCommune(pool, { id: toCommuneId('97300'), nom: 'Une ville en Guyane', geometry: '010100000000000000000000000000000000000000' })
   const entreprises = await entreprisesUpsert([{ id: newEntrepriseId('plop'), nom: 'Mon Entreprise' }])
-  await titreCreate(
-    {
-      nom: 'mon titre simple',
-      typeId: 'arm',
-      titreStatutId: 'val',
-      propsTitreEtapesIds: {},
-    },
-    {}
-  )
+  await insertTitreGraph({
+    id: newTitreId(),
+    nom: 'mon titre simple',
+    typeId: 'arm',
+    titreStatutId: 'val',
+    propsTitreEtapesIds: {},
+  })
 
   await createTitreWithEtapes(
     'titre1',
@@ -133,24 +132,23 @@ const titreEtapesCreate = async (demarche: ITitreDemarche, etapes: Omit<ITitreEt
 }
 
 async function createTitreWithEtapes(nomTitre: string, etapes: Omit<ITitreEtape, 'id' | 'titreDemarcheId'>[], entreprises: any) {
-  const titre = await titreCreate(
-    {
-      nom: nomTitre,
-      typeId: 'arm',
-      titreStatutId: 'mod',
-      propsTitreEtapesIds: {},
-      references: [
-        {
-          referenceTypeId: 'onf',
-          nom: 'ONF',
-        },
-      ],
-    },
-    {}
-  )
+  const titreId = newTitreId()
+  await insertTitreGraph({
+    id: titreId,
+    nom: nomTitre,
+    typeId: 'arm',
+    titreStatutId: 'mod',
+    propsTitreEtapesIds: {},
+    references: [
+      {
+        referenceTypeId: 'onf',
+        nom: 'ONF',
+      },
+    ],
+  })
 
   const titreDemarche = await titreDemarcheCreate({
-    titreId: titre.id,
+    titreId,
     typeId: 'oct',
   })
 
@@ -160,9 +158,9 @@ async function createTitreWithEtapes(nomTitre: string, etapes: Omit<ITitreEtape,
 
   await knex('titres')
     .update({ propsTitreEtapesIds: { titulaires: etapesCrees[0].id, points: etapesCrees[0].id } })
-    .where('id', titre.id)
+    .where('id', titreId)
 
-  return titre.id
+  return titreId
 }
 
 describe('titresAdministration', () => {
@@ -202,20 +200,20 @@ describe('titresLiaisons', () => {
     )
     const titreId = getTitres.body[0].id
 
-    const axm = await titreCreate(
-      {
-        nom: 'mon axm simple',
-        typeId: 'axm',
-        titreStatutId: 'val',
-        propsTitreEtapesIds: {},
-      },
-      {}
-    )
+    const axmId = newTitreId()
+    const axmNom = 'mon axm simple'
+    await insertTitreGraph({
+      id: axmId,
+      nom: axmNom,
+      typeId: 'axm',
+      titreStatutId: 'val',
+      propsTitreEtapesIds: {},
+    })
 
     const tested = await restNewPostCall(
       dbPool,
       '/rest/titres/:id/titreLiaisons',
-      { id: axm.id },
+      { id: axmId },
       {
         role: 'admin',
         administrationId: ADMINISTRATION_IDS['DGTM - GUYANE'],
@@ -245,8 +243,8 @@ describe('titresLiaisons', () => {
     expect(avalTested.body.amont).toHaveLength(0)
     expect(avalTested.body.aval).toHaveLength(1)
     expect(avalTested.body.aval[0]).toStrictEqual({
-      id: axm.id,
-      nom: axm.nom,
+      id: axmId,
+      nom: axmNom,
     })
   })
 })
@@ -255,16 +253,14 @@ describe('titreModifier', () => {
   let id = newTitreId('')
 
   beforeEach(async () => {
-    const titre = await titreCreate(
-      {
-        nom: 'mon titre',
-        typeId: 'arm',
-        titreStatutId: 'ind',
-        propsTitreEtapesIds: {},
-      },
-      {}
-    )
-    id = titre.id
+    id = newTitreId()
+    await insertTitreGraph({
+      id,
+      nom: 'mon titre',
+      typeId: 'arm',
+      titreStatutId: 'ind',
+      propsTitreEtapesIds: {},
+    })
   })
 
   test('ne peut pas modifier un titre (utilisateur anonyme)', async () => {
@@ -299,25 +295,18 @@ describe('titreModifier', () => {
   })
 
   test("ne peut pas modifier un titre ARM échu (un utilisateur 'admin' PTMG)", async () => {
-    const titre = await titreCreate(
-      {
-        nom: 'mon titre échu',
-        typeId: 'arm',
-        titreStatutId: 'ech',
-        propsTitreEtapesIds: {},
-      },
-      {}
-    )
+    const id = newTitreId()
+    await insertTitreGraph({ id, nom: 'mon titre échu', typeId: 'arm', titreStatutId: 'ech', propsTitreEtapesIds: {} })
 
     const tested = await restPostCall(
       dbPool,
       '/rest/titres/:titreId',
-      { titreId: titre.id },
+      { titreId: id },
       {
         role: 'admin',
         administrationId: ADMINISTRATION_IDS['PÔLE TECHNIQUE MINIER DE GUYANE'],
       },
-      { id: titre.id, nom: 'mon titre modifié', references: [] }
+      { id, nom: 'mon titre modifié', references: [] }
     )
     expect(tested.statusCode).toBe(404)
   })
@@ -338,16 +327,14 @@ describe('titreSupprimer', () => {
   let id = newTitreId('')
 
   beforeEach(async () => {
-    const titre = await titreCreate(
-      {
-        nom: 'mon titre',
-        typeId: 'arm',
-        titreStatutId: 'ind',
-        propsTitreEtapesIds: {},
-      },
-      {}
-    )
-    id = titre.id
+    id = newTitreId()
+    await insertTitreGraph({
+      id,
+      nom: 'mon titre',
+      typeId: 'arm',
+      titreStatutId: 'ind',
+      propsTitreEtapesIds: {},
+    })
   })
 
   test('ne peut pas supprimer un titre (utilisateur anonyme)', async () => {
@@ -538,18 +525,17 @@ describe('getTitre', () => {
 })
 
 test('utilisateurTitreAbonner', async () => {
-  const titre = await titreCreate(
-    {
-      nom: 'mon autre titre',
-      typeId: 'arm',
-      slug: titreSlugValidator.parse('slug'),
-      titreStatutId: 'val',
-      propsTitreEtapesIds: {},
-    },
-    {}
-  )
+  const id = newTitreId()
+  await insertTitreGraph({
+    id,
+    nom: 'mon autre titre',
+    typeId: 'arm',
+    slug: titreSlugValidator.parse('slug'),
+    titreStatutId: 'val',
+    propsTitreEtapesIds: {},
+  })
 
-  const tested = await restPostCall(dbPool, '/rest/titres/:titreId/abonne', { titreId: titre.id }, userSuper, { abonne: true })
+  const tested = await restPostCall(dbPool, '/rest/titres/:titreId/abonne', { titreId: id }, userSuper, { abonne: true })
 
   expect(tested.statusCode).toBe(HTTP_STATUS.NO_CONTENT)
 })
diff --git a/packages/api/src/api/rest/titres.ts b/packages/api/src/api/rest/titres.ts
index 6b287fb3e..0b00f425c 100644
--- a/packages/api/src/api/rest/titres.ts
+++ b/packages/api/src/api/rest/titres.ts
@@ -1,4 +1,4 @@
-import { titreArchive, titresGet, titreGet, titreUpsert } from '../../database/queries/titres'
+import { titreArchive, titresGet, titreGet, titreUpdate } from '../../database/queries/titres'
 import { HTTP_STATUS } from 'camino-common/src/http'
 import { CommonTitreAdministration, editableTitreValidator, TitreLink, TitreLinks, TitreGet, utilisateurTitreAbonneValidator } from 'camino-common/src/titres'
 import { machineFind } from '../../business/rules-demarches/definitions'
@@ -445,9 +445,7 @@ export const updateTitre =
           if (!canEditTitre(user, titreOld.typeId, titreOld.titreStatutId, titreOld.administrationsLocales ?? [])) {
             res.sendStatus(HTTP_STATUS.FORBIDDEN)
           } else {
-            // on doit utiliser upsert (plutôt qu'un simple update)
-            // car le titre contient des références (tableau d'objet)
-            await titreUpsert(parsedBody.data)
+            await titreUpdate(titreId, parsedBody.data)
 
             await titreUpdateTask(pool, titreId)
             res.sendStatus(HTTP_STATUS.NO_CONTENT)
diff --git a/packages/api/src/business/daily.ts b/packages/api/src/business/daily.ts
index a60c12c26..60c7292f1 100644
--- a/packages/api/src/business/daily.ts
+++ b/packages/api/src/business/daily.ts
@@ -20,6 +20,8 @@ import type { Pool } from 'pg'
 import { demarchesDefinitionsCheck } from '../tools/demarches/definitions-check'
 import { titreTypeDemarcheTypeEtapeTypeCheck } from '../tools/demarches/tde-check'
 import { titresEtapesStatutUpdate } from './processes/titres-etapes-statut-update'
+import { callAndExit } from '../tools/fp-tools'
+import { etapesFondamentaleIdUpdateForAll } from './processes/titres-etapes-fondamentale-id-update'
 
 export const daily = async (pool: Pool): Promise<void> => {
   try {
@@ -29,6 +31,7 @@ export const daily = async (pool: Pool): Promise<void> => {
 
     const titresEtapesStatusUpdated = await titresEtapesStatutUpdate(pool)
     const titresEtapesOrdreUpdated = await titresEtapesOrdreUpdate(pool, userSuper)
+    await callAndExit(etapesFondamentaleIdUpdateForAll(pool), async () => {})
     const titresEtapesHeritagePropsUpdated = await titresEtapesHeritagePropsUpdate(userSuper)
     const titresEtapesHeritageContenuUpdated = await titresEtapesHeritageContenuUpdate(pool, userSuper)
 
diff --git a/packages/api/src/business/processes/__snapshots__/titres-etapes-areas-update.test.integration.ts.snap b/packages/api/src/business/processes/__snapshots__/titres-etapes-areas-update.test.integration.ts.snap
index 0160e0bd8..b7c206423 100644
--- a/packages/api/src/business/processes/__snapshots__/titres-etapes-areas-update.test.integration.ts.snap
+++ b/packages/api/src/business/processes/__snapshots__/titres-etapes-areas-update.test.integration.ts.snap
@@ -21,6 +21,7 @@ exports[`titresEtapesAreasUpdate > met à jour les communes, forêts et zone du
     "dateDebut": null,
     "dateFin": null,
     "duree": null,
+    "etapeFondamentaleId": "titreEtapeIdUniquePourMiseAJourAreas",
     "forets": [
       "DBR",
     ],
@@ -159,6 +160,7 @@ exports[`titresEtapesAreasUpdate > met à jour les communes, forêts et zone du
       "dateDebut": null,
       "dateFin": null,
       "duree": null,
+      "etapeFondamentaleId": "titreEtapeIdUniquePourMiseAJourAreas",
       "forets": [
         "DBR",
       ],
diff --git a/packages/api/src/business/processes/titres-activites-props-update.test.ts b/packages/api/src/business/processes/titres-activites-props-update.test.ts
index f607348a0..bb7482125 100644
--- a/packages/api/src/business/processes/titres-activites-props-update.test.ts
+++ b/packages/api/src/business/processes/titres-activites-props-update.test.ts
@@ -1,5 +1,5 @@
 import { titresActivitesPropsUpdate } from './titres-activites-props-update'
-import { titresActivitesUpsert } from '../../database/queries/titres-activites'
+import { titreActiviteUpdate } from '../../database/queries/titres-activites'
 import { titresGet } from '../../database/queries/titres'
 import { titreValideCheck } from '../utils/titre-valide-check'
 import { vi, describe, expect, test, afterEach } from 'vitest'
@@ -9,7 +9,7 @@ import { toCaminoDate } from 'camino-common/src/date'
 import { activiteIdValidator } from 'camino-common/src/activite'
 
 vi.mock('../../database/queries/titres-activites', () => ({
-  titresActivitesUpsert: vi.fn(),
+  titreActiviteUpdate: vi.fn(),
 }))
 
 vi.mock('../../database/queries/titres', () => ({
@@ -20,7 +20,7 @@ vi.mock('../utils/titre-valide-check', () => ({
   titreValideCheck: vi.fn(),
 }))
 
-const titresActivitesUpsertMock = vi.mocked(titresActivitesUpsert, true)
+const titreActiviteUpdateMock = vi.mocked(titreActiviteUpdate, true)
 const titresGetMock = vi.mocked(titresGet, true)
 const titreValideCheckMock = vi.mocked(titreValideCheck, true)
 
@@ -94,7 +94,7 @@ describe("propriété des activités d'un titre", () => {
     const titresActivitesUpdated = await titresActivitesPropsUpdate()
 
     expect(titresActivitesUpdated).toEqual(['titre-activite-id-2019-04', 'titre-activite-id-2020-01'])
-    expect(titresActivitesUpsertMock).toHaveBeenCalled()
+    expect(titreActiviteUpdateMock).toHaveBeenCalled()
   })
   test("ne met pas à jour la propriété suppression d'une activité", async () => {
     const titresActivitesNotToUpdate: ITitre[] = [
@@ -159,6 +159,6 @@ describe("propriété des activités d'un titre", () => {
     const titresActivitesUpdated = await titresActivitesPropsUpdate()
 
     expect(titresActivitesUpdated).toEqual([])
-    expect(titresActivitesUpsertMock).not.toHaveBeenCalled()
+    expect(titreActiviteUpdateMock).not.toHaveBeenCalled()
   })
 })
diff --git a/packages/api/src/business/processes/titres-activites-props-update.ts b/packages/api/src/business/processes/titres-activites-props-update.ts
index a8567c75d..24b21851f 100644
--- a/packages/api/src/business/processes/titres-activites-props-update.ts
+++ b/packages/api/src/business/processes/titres-activites-props-update.ts
@@ -1,16 +1,17 @@
 import type { ITitreActivite } from '../../types'
 
-import { titresActivitesUpsert } from '../../database/queries/titres-activites'
+import { titreActiviteUpdate } from '../../database/queries/titres-activites'
 import { titresGet } from '../../database/queries/titres'
 import { titreValideCheck } from '../utils/titre-valide-check'
 import { userSuper } from '../../database/user-super'
 import { getMonth } from 'camino-common/src/static/frequence'
 import { toCaminoDate } from 'camino-common/src/date'
 import { ActivitesTypes } from 'camino-common/src/static/activitesTypes'
-import { isNotNullNorUndefinedNorEmpty, isNullOrUndefinedOrEmpty } from 'camino-common/src/typescript-tools'
+import { isNotNullNorUndefinedNorEmpty } from 'camino-common/src/typescript-tools'
+import { ActiviteId } from 'camino-common/src/activite'
 
 // TODO 2023-04-12 à supprimer et à calculer lors de l’appel à l’API par un super
-export const titresActivitesPropsUpdate = async (titresIds?: string[]) => {
+export const titresActivitesPropsUpdate = async (titresIds?: string[]): Promise<ActiviteId[]> => {
   console.info()
   console.info('propriétés des activités de titres…')
 
@@ -25,12 +26,8 @@ export const titresActivitesPropsUpdate = async (titresIds?: string[]) => {
     userSuper
   )
 
-  const titresActivitesUpdated = titres.reduce((acc: ITitreActivite[], titre) => {
-    if (isNullOrUndefinedOrEmpty(titre.activites)) {
-      return acc
-    }
-
-    return titre.activites.reduce((acc, titreActivite) => {
+  const titresActivitesUpdated = titres.flatMap(titre => {
+    return (titre.activites ?? []).reduce<Pick<ITitreActivite, 'id' | 'suppression'>[]>((acc, titreActivite) => {
       const activiteType = ActivitesTypes[titreActivite.typeId]
 
       const dateDebut = toCaminoDate(new Date(titreActivite.annee, getMonth(activiteType.frequenceId, titreActivite.periodeId), 1))
@@ -38,23 +35,19 @@ export const titresActivitesPropsUpdate = async (titresIds?: string[]) => {
       const isActiviteInPhase: boolean = isNotNullNorUndefinedNorEmpty(titre.demarches) && titreValideCheck(titre.demarches, dateDebut, titreActivite.date)
 
       if (isActiviteInPhase && (titreActivite.suppression ?? false)) {
-        titreActivite.suppression = false
-
-        acc.push(titreActivite)
+        acc.push({ id: titreActivite.id, suppression: false })
       }
 
       if (!isActiviteInPhase && !(titreActivite.suppression ?? false)) {
-        titreActivite.suppression = true
-
-        acc.push(titreActivite)
+        acc.push({ id: titreActivite.id, suppression: true })
       }
 
       return acc
-    }, acc)
-  }, [])
+    }, [])
+  })
 
   if (titresActivitesUpdated.length) {
-    await titresActivitesUpsert(titresActivitesUpdated)
+    await Promise.all(titresActivitesUpdated.map(activite => titreActiviteUpdate(activite.id, activite)))
 
     console.info('titre / activités / propriétés (mise à jour) ->', titresActivitesUpdated.map(ta => ta.id).join(', '))
   }
diff --git a/packages/api/src/business/processes/titres-activites-update.test.ts b/packages/api/src/business/processes/titres-activites-update.test.ts
index 8513a9ee5..373f275d7 100644
--- a/packages/api/src/business/processes/titres-activites-update.test.ts
+++ b/packages/api/src/business/processes/titres-activites-update.test.ts
@@ -3,7 +3,7 @@ import { ITitreActivite } from '../../types'
 import { titresActivitesUpdate } from './titres-activites-update'
 import { canHaveActiviteTypeId } from 'camino-common/src/permissions/titres'
 import { anneesBuild } from '../../tools/annees-build'
-import { titresActivitesUpsert } from '../../database/queries/titres-activites'
+import { titresActivitesInsert } from '../../database/queries/titres-activites'
 import { titresGet } from '../../database/queries/titres'
 import { titreActivitesBuild } from '../rules/titre-activites-build'
 
@@ -32,7 +32,7 @@ vi.mock('../../tools/annees-build', () => ({
 
 vi.mock('../../database/queries/titres-activites', () => ({
   __esModule: true,
-  titresActivitesUpsert: vi.fn().mockResolvedValue(true),
+  titresActivitesInsert: vi.fn().mockResolvedValue(true),
 }))
 
 vi.mock('../rules/titre-activites-build', () => ({
@@ -110,7 +110,7 @@ describe("activités d'un titre", () => {
     expect(titresActivitesNew.length).toEqual(8)
 
     expect(canHaveActiviteTypeId).toHaveBeenCalledTimes(8)
-    expect(titresActivitesUpsert).toHaveBeenCalled()
+    expect(titresActivitesInsert).toHaveBeenCalled()
     expect(titreActivitesBuild).toHaveBeenCalled()
     expect(getEntrepriseUtilisateurs).toHaveBeenCalled()
     expect(emailsWithTemplateSendMock).toHaveBeenCalledWith(['email'], EmailTemplateId.ACTIVITES_NOUVELLES, expect.any(Object))
@@ -128,7 +128,7 @@ describe("activités d'un titre", () => {
 
     expect(canHaveActiviteTypeId).toHaveBeenCalledTimes(8)
     expect(titreActivitesBuild).toHaveBeenCalled()
-    expect(titresActivitesUpsert).not.toHaveBeenCalled()
+    expect(titresActivitesInsert).not.toHaveBeenCalled()
     expect(getEntrepriseUtilisateurs).not.toHaveBeenCalled()
     expect(emailsSendMock).not.toHaveBeenCalled()
   })
@@ -144,7 +144,7 @@ describe("activités d'un titre", () => {
 
     expect(canHaveActiviteTypeId).toHaveBeenCalledTimes(8)
     expect(titreActivitesBuild).not.toHaveBeenCalled()
-    expect(titresActivitesUpsert).not.toHaveBeenCalled()
+    expect(titresActivitesInsert).not.toHaveBeenCalled()
     expect(getEntrepriseUtilisateurs).not.toHaveBeenCalled()
   })
 
@@ -159,7 +159,7 @@ describe("activités d'un titre", () => {
 
     expect(canHaveActiviteTypeId).not.toHaveBeenCalled()
     expect(titreActivitesBuild).not.toHaveBeenCalled()
-    expect(titresActivitesUpsert).not.toHaveBeenCalled()
+    expect(titresActivitesInsert).not.toHaveBeenCalled()
     expect(getEntrepriseUtilisateurs).not.toHaveBeenCalled()
   })
 })
diff --git a/packages/api/src/business/processes/titres-activites-update.ts b/packages/api/src/business/processes/titres-activites-update.ts
index 67fbf14e6..11c8b9552 100644
--- a/packages/api/src/business/processes/titres-activites-update.ts
+++ b/packages/api/src/business/processes/titres-activites-update.ts
@@ -1,7 +1,7 @@
 import { ITitreActivite } from '../../types'
 
 import { anneesBuild } from '../../tools/annees-build'
-import { titresActivitesUpsert } from '../../database/queries/titres-activites'
+import { titresActivitesInsert } from '../../database/queries/titres-activites'
 import { titreActivitesBuild } from '../rules/titre-activites-build'
 import { titresGet } from '../../database/queries/titres'
 import { userSuper } from '../../database/user-super'
@@ -36,38 +36,34 @@ export const titresActivitesUpdate = async (pool: Pool, titresIds?: string[], au
     userSuper
   )
 
-  const titresActivitesCreated = sortedActivitesTypes.reduce((acc: ITitreActivite[], activiteType) => {
+  const titresActivitesCreated = sortedActivitesTypes.flatMap(activiteType => {
     const annees = anneesBuild(activiteType.dateDebut, aujourdhui)
-    if (!annees.length) return acc
+    if (!annees.length) return []
 
-    acc.push(
-      ...titres.reduce((acc: ITitreActivite[], titre) => {
-        if (isNullOrUndefined(titre.demarches)) {
-          throw new Error('les démarches du titre ne sont pas chargées')
-        }
-
-        if (isNullOrUndefined(titre.communes)) {
-          throw new Error('les communes du titre ne sont pas chargées')
-        }
+    return titres.reduce((acc: ITitreActivite[], titre) => {
+      if (isNullOrUndefined(titre.demarches)) {
+        throw new Error('les démarches du titre ne sont pas chargées')
+      }
 
-        if (isNullOrUndefined(titre.secteursMaritime)) {
-          throw new Error('les secteursMaritime du titre ne sont pas chargés')
-        }
+      if (isNullOrUndefined(titre.communes)) {
+        throw new Error('les communes du titre ne sont pas chargées')
+      }
 
-        // si le type d'activité est relié au type de titre
-        if (!canHaveActiviteTypeId(activiteType.id, { titreTypeId: titre.typeId, communes: titre.communes, secteursMaritime: titre.secteursMaritime, demarches: titre.demarches })) return acc
+      if (isNullOrUndefined(titre.secteursMaritime)) {
+        throw new Error('les secteursMaritime du titre ne sont pas chargés')
+      }
 
-        acc.push(...titreActivitesBuild(activiteType.id, annees, aujourdhui, titre.id, titre.typeId, titre.demarches, titre.activites))
+      // si le type d'activité est relié au type de titre
+      if (!canHaveActiviteTypeId(activiteType.id, { titreTypeId: titre.typeId, communes: titre.communes, secteursMaritime: titre.secteursMaritime, demarches: titre.demarches })) return acc
 
-        return acc
-      }, [])
-    )
+      acc.push(...titreActivitesBuild(activiteType.id, annees, aujourdhui, titre.id, titre.typeId, titre.demarches, titre.activites))
 
-    return acc
-  }, [])
+      return acc
+    }, [])
+  })
 
   if (titresActivitesCreated.length) {
-    await titresActivitesUpsert(titresActivitesCreated)
+    await titresActivitesInsert(titresActivitesCreated)
 
     const emails = new Set<string>()
     for (const activite of titresActivitesCreated) {
diff --git a/packages/api/src/business/processes/titres-etapes-fondamentale-id-update.ts b/packages/api/src/business/processes/titres-etapes-fondamentale-id-update.ts
new file mode 100644
index 000000000..fb7ddcd7d
--- /dev/null
+++ b/packages/api/src/business/processes/titres-etapes-fondamentale-id-update.ts
@@ -0,0 +1,50 @@
+import { DemarcheId } from 'camino-common/src/demarche'
+import { CaminoError } from 'camino-common/src/zod-tools'
+import { pipe, Effect } from 'effect'
+import { Pool } from 'pg'
+import { getDemarches, getEtapesByDemarcheId } from '../../api/rest/demarches.queries'
+import { EtapesTypes } from 'camino-common/src/static/etapesTypes'
+import { EtapeId } from 'camino-common/src/etape'
+import { updateEtapeFondamentaleId } from '../../database/queries/titres-etapes.queries'
+import { EffectDbQueryAndValidateErrors } from '../../pg-database'
+import { shortCircuitError } from '../../tools/fp-tools'
+import { isNotNullNorUndefinedNorEmpty, toSorted } from 'camino-common/src/typescript-tools'
+
+type EtapesFondamentaleIdUpdateErrors = EffectDbQueryAndValidateErrors
+export const etapesFondamentaleIdUpdate = (pool: Pool, demarcheId: DemarcheId): Effect.Effect<number, CaminoError<EtapesFondamentaleIdUpdateErrors>> =>
+  pipe(
+    getEtapesByDemarcheId(pool, demarcheId),
+    Effect.filterOrFail(
+      etapes => isNotNullNorUndefinedNorEmpty(etapes),
+      () => shortCircuitError("Pas d'étape pour cette démarche")
+    ),
+    Effect.map(etapes => toSorted(etapes, (a, b) => a.ordre - b.ordre)),
+    Effect.map(etapes => {
+      const result: { id: EtapeId; oldEtapeFondamentaleId: EtapeId; newEtapeFondamentaleId: EtapeId }[] = []
+      let etapeFondamentaleId = etapes[0].id
+      for (const currentEtape of etapes) {
+        if (EtapesTypes[currentEtape.etape_type_id].fondamentale) {
+          etapeFondamentaleId = currentEtape.id
+        }
+        if (currentEtape.etape_fondamentale_id !== etapeFondamentaleId) {
+          result.push({ id: currentEtape.id, newEtapeFondamentaleId: etapeFondamentaleId, oldEtapeFondamentaleId: currentEtape.etape_fondamentale_id })
+        }
+      }
+      return result
+    }),
+    Effect.flatMap(
+      Effect.forEach(etapeToUpdate => {
+        console.info(`maj étape fondamentale id ${etapeToUpdate.id} : ${etapeToUpdate.oldEtapeFondamentaleId} --> ${etapeToUpdate.newEtapeFondamentaleId}`)
+        return updateEtapeFondamentaleId(pool, etapeToUpdate.id, etapeToUpdate.newEtapeFondamentaleId)
+      })
+    ),
+    Effect.map(etapes => etapes.length),
+    Effect.catchTag("Pas d'étape pour cette démarche", _myError => Effect.succeed(0))
+  )
+
+export const etapesFondamentaleIdUpdateForAll = (pool: Pool): Effect.Effect<number, CaminoError<EtapesFondamentaleIdUpdateErrors>> =>
+  pipe(
+    getDemarches(pool),
+    Effect.flatMap(Effect.forEach(({ id }) => etapesFondamentaleIdUpdate(pool, id))),
+    Effect.map(demarches => demarches.length)
+  )
diff --git a/packages/api/src/business/titre-etape-update.ts b/packages/api/src/business/titre-etape-update.ts
index fedd31dcd..a20910d82 100644
--- a/packages/api/src/business/titre-etape-update.ts
+++ b/packages/api/src/business/titre-etape-update.ts
@@ -23,6 +23,8 @@ import { titresEtapesDepotCreate } from './processes/titres-demarches-depot-crea
 import type { UserNotNull } from 'camino-common/src/roles'
 import type { Pool } from 'pg'
 import { DeepReadonly } from 'camino-common/src/typescript-tools'
+import { callAndExit } from '../tools/fp-tools'
+import { etapesFondamentaleIdUpdate } from './processes/titres-etapes-fondamentale-id-update'
 
 export const titreEtapeUpdateTask = async (pool: Pool, titreEtapeId: EtapeId | null, titreDemarcheId: DemarcheId, user: DeepReadonly<UserNotNull>): Promise<void> => {
   try {
@@ -43,6 +45,7 @@ export const titreEtapeUpdateTask = async (pool: Pool, titreEtapeId: EtapeId | n
     }
 
     const titresEtapesOrdreUpdated = await titresEtapesOrdreUpdate(pool, user, titreDemarcheId)
+    await callAndExit(etapesFondamentaleIdUpdate(pool, titreDemarcheId), async () => {})
 
     const titresEtapesHeritagePropsUpdated = await titresEtapesHeritagePropsUpdate(user, [titreDemarcheId])
     const titresEtapesHeritageContenuUpdated = await titresEtapesHeritageContenuUpdate(pool, user, titreDemarcheId)
diff --git a/packages/api/src/business/utils/titre-slug-and-relations-update.test.integration.ts b/packages/api/src/business/utils/titre-slug-and-relations-update.test.integration.ts
index 4b1662b1d..838a53904 100644
--- a/packages/api/src/business/utils/titre-slug-and-relations-update.test.integration.ts
+++ b/packages/api/src/business/utils/titre-slug-and-relations-update.test.integration.ts
@@ -1,5 +1,5 @@
 import { titreSlugAndRelationsUpdate } from './titre-slug-and-relations-update'
-import { titreCreate, titreGet } from '../../database/queries/titres'
+import { titreGet } from '../../database/queries/titres'
 import { userSuper } from '../../database/user-super'
 import { dbManager } from '../../../tests/db-manager'
 import { ITitre } from '../../types'
@@ -7,6 +7,10 @@ import Titres from '../../database/models/titres'
 import { objectClone } from '../../tools/index'
 import { expect, test, describe, afterAll, beforeAll } from 'vitest'
 import { titreSlugValidator } from 'camino-common/src/validators/titres'
+import { insertTitreGraph } from '../../../tests/integration-test-helper'
+import { isNullOrUndefined } from 'camino-common/src/typescript-tools'
+import { newDemarcheId, newTitreId } from '../../database/models/_format/id-create'
+import { demarcheSlugValidator } from 'camino-common/src/demarche'
 beforeAll(async () => {
   await dbManager.populateDb()
 })
@@ -15,28 +19,42 @@ afterAll(async () => {
   await dbManager.closeKnex()
 })
 
-const titreAdd = async (titre: ITitre) =>
-  titreCreate(titre, {
-    fields: {
-      demarches: {
-        etapes: {
-          id: {},
+const titreAdd = async (titre: ITitre): Promise<ITitre> => {
+  await insertTitreGraph(titre)
+  const savedTitre = await titreGet(
+    titre.id,
+    {
+      fields: {
+        demarches: {
+          etapes: {
+            id: {},
+          },
         },
+        activites: { id: {} },
       },
-      activites: { id: {} },
     },
-  })
+    userSuper
+  )
+
+  if (isNullOrUndefined(savedTitre)) {
+    throw new Error('on a pas réussi à sauvegarder le titre')
+  }
+
+  return savedTitre
+}
 
 describe('vérifie la mis à jour des slugs sur les relations d’un titre', () => {
   test('met à jour le slug d’un nouveau titre', async () => {
     await Titres.query().delete()
 
     const titre = await titreAdd({
+      id: newTitreId(),
       nom: 'titre-nom',
       typeId: 'arm',
+      titreStatutId: 'ech',
       propsTitreEtapesIds: {},
-      slug: 'toto',
-    } as ITitre)
+      slug: titreSlugValidator.parse('toto'),
+    })
 
     const { hasChanged, slug } = await titreSlugAndRelationsUpdate(titre)
     expect(hasChanged).toEqual(true)
@@ -50,11 +68,13 @@ describe('vérifie la mis à jour des slugs sur les relations d’un titre', ()
     await Titres.query().delete()
 
     const titre = await titreAdd({
+      id: newTitreId(),
       nom: 'titre-nom',
       typeId: 'arm',
+      titreStatutId: 'ech',
       propsTitreEtapesIds: {},
-      slug: 'm-ar-titre-nom-0000',
-    } as ITitre)
+      slug: titreSlugValidator.parse('m-ar-titre-nom-0000'),
+    })
 
     const { hasChanged, slug } = await titreSlugAndRelationsUpdate(titre)
     expect(hasChanged).toEqual(false)
@@ -67,15 +87,17 @@ describe('vérifie la mis à jour des slugs sur les relations d’un titre', ()
   test('génère un slug différent si le slug existe déjà', async () => {
     await Titres.query().delete()
 
-    const titrePojo = {
+    const titrePojo: ITitre = {
+      id: newTitreId(),
       nom: 'titre-nom',
       typeId: 'arm',
+      titreStatutId: 'ech',
       propsTitreEtapesIds: {},
-    } as ITitre
+    }
 
     let titre = await titreAdd(objectClone(titrePojo))
     const { slug: firstSlug } = await titreSlugAndRelationsUpdate(titre)
-    titre = await titreAdd(titrePojo)
+    titre = await titreAdd({ ...titrePojo, id: newTitreId() })
 
     const { hasChanged, slug: secondSlug } = await titreSlugAndRelationsUpdate(titre)
     expect(hasChanged).toEqual(true)
@@ -86,16 +108,18 @@ describe('vérifie la mis à jour des slugs sur les relations d’un titre', ()
   test('ne modifie pas le hash d’un slug déjà en double', async () => {
     await Titres.query().delete()
 
-    const titrePojo = {
+    const titrePojo: ITitre = {
+      id: newTitreId(),
       nom: 'titre-nom',
       typeId: 'arm',
+      titreStatutId: 'ech',
       propsTitreEtapesIds: {},
-    } as ITitre
+    }
 
     let titre = await titreAdd(objectClone(titrePojo))
     const firstTitreId = titre.id
     const { slug: firstSlug } = await titreSlugAndRelationsUpdate(titre)
-    titre = await titreAdd({ ...titrePojo, slug: titreSlugValidator.parse(`${firstSlug}-123123`) })
+    titre = await titreAdd({ ...titrePojo, id: newTitreId(), slug: titreSlugValidator.parse(`${firstSlug}-123123`) })
 
     const { hasChanged, slug: secondSlug } = await titreSlugAndRelationsUpdate(titre)
     expect(hasChanged).toEqual(true)
@@ -107,20 +131,24 @@ describe('vérifie la mis à jour des slugs sur les relations d’un titre', ()
 
   test('génère un slug pour une démarche', async () => {
     await Titres.query().delete()
-
+    const id = newTitreId()
     const titre = await titreAdd({
       nom: 'titre-nom',
+      id,
       typeId: 'arm',
+      titreStatutId: 'ech',
       propsTitreEtapesIds: {},
-      slug: 'm-ar-titre-nom-0000',
+      slug: titreSlugValidator.parse('m-ar-titre-nom-0000'),
       demarches: [
         {
+          id: newDemarcheId(),
+          titreId: id,
           typeId: 'oct',
           statutId: 'dep',
-          slug: 'slug',
+          slug: demarcheSlugValidator.parse('slug'),
         },
       ],
-    } as ITitre)
+    })
 
     const { slug, hasChanged } = await titreSlugAndRelationsUpdate(titre)
 
diff --git a/packages/api/src/business/utils/titre-slug-and-relations-update.ts b/packages/api/src/business/utils/titre-slug-and-relations-update.ts
index 18346fb89..e16d87185 100644
--- a/packages/api/src/business/utils/titre-slug-and-relations-update.ts
+++ b/packages/api/src/business/utils/titre-slug-and-relations-update.ts
@@ -44,7 +44,7 @@ const titreActiviteSlugFind = (titreActivite: ITitreActivite, titre: ITitre) =>
 interface ITitreRelation<T extends string | DemarcheId = string> {
   name: string
   slugFind: (...args: any[]) => string
-  update: ((id: T, element: { slug: string }, user: UserNotNull) => Promise<{ id: T }>) | ((id: T, element: { slug: string }, user: UserNotNull, titreId: TitreId) => Promise<{ id: T }>)
+  update: ((id: T, element: { slug: string }, user: UserNotNull) => Promise<unknown>) | ((id: T, element: { slug: string }, user: UserNotNull, titreId: TitreId) => Promise<unknown>)
   relations?: ITitreRelation[]
 }
 
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 006b0502c..b007e56b8 100644
--- a/packages/api/src/business/validations/titre-demarche-etat-validate.ts
+++ b/packages/api/src/business/validations/titre-demarche-etat-validate.ts
@@ -1,6 +1,6 @@
 // valide la date et la position de l'étape en fonction des autres étapes
 import { DeepReadonly, NonEmptyArray, isNonEmptyArray, isNotNullNorUndefined, isNullOrUndefined, isNullOrUndefinedOrEmpty } from 'camino-common/src/typescript-tools'
-import type { ITitre, ITitreEtape } from '../../types'
+import type { ITitreEtape } from '../../types'
 
 import { machineFind } from '../rules-demarches/definitions'
 import { Etape, TitreEtapeForMachine, titreEtapeForMachineValidator, toMachineEtapes } from '../rules-demarches/machine-common'
@@ -48,7 +48,7 @@ const titreDemarcheEtapesBuild = <T extends Pick<Partial<ITitreEtape>, 'id'>>(ti
 // est valide par rapport aux définitions des types d'étape
 export const titreDemarcheUpdatedEtatValidate = (
   demarcheTypeId: DemarcheTypeId,
-  titre: Pick<ITitre, 'typeId' | 'demarches'>,
+  titre: { typeId: TitreTypeId; demarches?: { typeId: DemarcheTypeId }[] },
   titreEtape: Pick<Partial<ITitreEtape>, 'id'> & Pick<ITitreEtape, 'statutId' | 'typeId' | 'date' | 'contenu' | 'surface' | 'communes' | 'isBrouillon'>,
   demarcheId: DemarcheId,
   titreDemarcheEtapes?: Pick<ITitreEtape, 'id' | 'statutId' | 'typeId' | 'date' | 'ordre' | 'contenu' | 'communes' | 'surface' | 'isBrouillon'>[] | null,
diff --git a/packages/api/src/database/models/titres-etapes.ts b/packages/api/src/database/models/titres-etapes.ts
index 684f8716f..0807af18e 100644
--- a/packages/api/src/database/models/titres-etapes.ts
+++ b/packages/api/src/database/models/titres-etapes.ts
@@ -9,11 +9,12 @@ import { heritagePropsFormat, heritageContenuFormat } from './_format/titre-etap
 import { idGenerate } from './_format/id-create'
 import TitresDemarches from './titres-demarches'
 import Journaux from './journaux'
-import { etapeSlugValidator } from 'camino-common/src/etape'
-import { isNotNullNorUndefined } from 'camino-common/src/typescript-tools'
+import { EtapeId, etapeSlugValidator } from 'camino-common/src/etape'
+import { isNotNullNorUndefined, isNullOrUndefined } from 'camino-common/src/typescript-tools'
 
 export interface DBTitresEtapes extends ITitreEtape {
   archive: boolean
+  etapeFondamentaleId: EtapeId
 }
 interface TitresEtapes extends DBTitresEtapes {}
 class TitresEtapes extends Model {
@@ -78,6 +79,9 @@ class TitresEtapes extends Model {
     if (!this.id) {
       this.id = idGenerate()
     }
+    if (isNullOrUndefined(this.etapeFondamentaleId)) {
+      this.etapeFondamentaleId = this.id
+    }
 
     // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
     if (!this.slug && this.titreDemarcheId && this.typeId) {
diff --git a/packages/api/src/database/queries/entreprises-etablissements.ts b/packages/api/src/database/queries/entreprises-etablissements.ts
index ee66834a4..1bb78bae3 100644
--- a/packages/api/src/database/queries/entreprises-etablissements.ts
+++ b/packages/api/src/database/queries/entreprises-etablissements.ts
@@ -7,6 +7,7 @@ import EntrepriseEtablissements from '../models/entreprises-etablissements'
 export const entreprisesEtablissementsGet = async () => EntrepriseEtablissements.query()
 
 export const entreprisesEtablissementsUpsert = async (entreprisesEtablissements: IEntrepriseEtablissement[]) =>
+  // eslint-disable-next-line no-restricted-syntax
   EntrepriseEtablissements.query().upsertGraph(entreprisesEtablissements, options.entreprisesEtablissements.update)
 
 export const entreprisesEtablissementsDelete = async (entreprisesEtablissementsIds: string[]) => EntrepriseEtablissements.query().delete().whereIn('id', entreprisesEtablissementsIds)
diff --git a/packages/api/src/database/queries/entreprises.ts b/packages/api/src/database/queries/entreprises.ts
index 437410e2a..f84db1d99 100644
--- a/packages/api/src/database/queries/entreprises.ts
+++ b/packages/api/src/database/queries/entreprises.ts
@@ -110,6 +110,10 @@ export const entreprisesGet = async (
   return q
 }
 
-export const entreprisesUpsert = async (entreprises: IEntreprise[]) => Entreprises.query().withGraphFetched(options.entreprises.graph).upsertGraph(entreprises, options.entreprises.update)
+export const entreprisesUpsert = async (entreprises: IEntreprise[]): Promise<IEntreprise[]> =>
+  // eslint-disable-next-line no-restricted-syntax
+  Entreprises.query().withGraphFetched(options.entreprises.graph).upsertGraph(entreprises, options.entreprises.update)
 
-export const entrepriseUpsert = async (entreprise: IEntreprise) => Entreprises.query().withGraphFetched(options.entreprises.graph).upsertGraph(entreprise, options.entreprises.update).returning('*')
+export const entrepriseUpsert = async (entreprise: Omit<IEntreprise, 'etablissements'>): Promise<IEntreprise> =>
+  // eslint-disable-next-line no-restricted-syntax
+  Entreprises.query().withGraphFetched(options.entreprises.graph).upsertGraph(entreprise, options.entreprises.update).returning('*')
diff --git a/packages/api/src/database/queries/journaux.ts b/packages/api/src/database/queries/journaux.ts
index 406bddf65..019558829 100644
--- a/packages/api/src/database/queries/journaux.ts
+++ b/packages/api/src/database/queries/journaux.ts
@@ -1,12 +1,12 @@
 import Journaux from '../models/journaux'
 import { create } from 'jsondiffpatch'
-import { Model, PartialModelGraph, RelationExpression, UpsertGraphOptions } from 'objection'
+import Objection, { Model, PartialModelGraph } from 'objection'
 import { journauxQueryModify } from './permissions/journaux'
 import { ITitreEtape } from '../../types'
 import graphBuild from './graph/build'
 import { fieldsFormat } from './graph/fields-format'
 import options, { FieldId } from './_options'
-import { User } from 'camino-common/src/roles'
+import { User, UtilisateurId } from 'camino-common/src/roles'
 import { TitreId } from 'camino-common/src/validators/titres'
 import { JournauxQueryParams } from 'camino-common/src/journaux'
 import TitresEtapes from '../models/titres-etapes'
@@ -18,7 +18,7 @@ const diffPatcher = create({
   propertyFilter: (name: string) => !['slug', 'ordre', 'demarche', 'heritageProps'].includes(name),
 })
 
-export const journauxGet = async (params: JournauxQueryParams, { fields }: { fields?: FieldId }, user: User) => {
+export const journauxGet = async (params: JournauxQueryParams, { fields }: { fields?: FieldId }, user: User): Promise<Objection.Page<Journaux>> => {
   const graph = fields ? graphBuild(fields, 'journaux', fieldsFormat) : options.journaux.graph
 
   const q = Journaux.query().withGraphFetched(graph)
@@ -38,7 +38,7 @@ export const journauxGet = async (params: JournauxQueryParams, { fields }: { fie
   return q.page(params.page - 1, 10)
 }
 
-export const createJournalCreate = async (id: string, userId: string, titreId: TitreId) => {
+export const createJournalCreate = async (id: string, userId: string, titreId: TitreId): Promise<void> => {
   await Journaux.query().insert({
     elementId: id,
     operation: 'create',
@@ -76,22 +76,19 @@ export const patchJournalCreate = async (id: EtapeId, partialEntity: Partial<ITi
   return result
 }
 
-export const upsertJournalCreate = async (
-  id: string | undefined,
-  entity: PartialModelGraph<ITitreEtape>,
-  options: UpsertGraphOptions,
-  relations: RelationExpression<TitresEtapes>,
-  userId: string,
-  titreId: TitreId
-): Promise<ITitreEtape | undefined> => {
+export const upsertJournalCreate = async (id: EtapeId | undefined, entity: PartialModelGraph<ITitreEtape>, userId: UtilisateurId, titreId: TitreId): Promise<ITitreEtape | undefined> => {
+  const relations = '[]'
   const oldValue = isNotNullNorUndefined(id) ? await TitresEtapes.query().findById(id).withGraphFetched(relations).returning('*') : undefined
 
-  // BUG Objection
-  // on ne peut pas utiliser returning('*'),
-  // car certains attributs de entity restent présents alors qu’ils sont enlevés avant l’enregistrement
-  const newModel = await TitresEtapes.query().upsertGraph(entity, options).returning('id')
+  let newModelId: EtapeId | undefined = oldValue?.id
 
-  const newValue = await TitresEtapes.query().findById(newModel.id).withGraphFetched(relations).returning('*')
+  if (isNullOrUndefined(newModelId)) {
+    newModelId = (await TitresEtapes.query().insert(entity).returning('id')).id
+  } else {
+    await TitresEtapes.query().update(entity).where('id', newModelId)
+  }
+
+  const newValue = await TitresEtapes.query().findById(newModelId).withGraphFetched(relations).returning('*')
 
   let differences: any
   let operation: 'create' | 'update' = 'create'
diff --git a/packages/api/src/database/queries/permissions/administrations.test.integration.ts b/packages/api/src/database/queries/permissions/administrations.test.integration.ts
index bfdc174e4..b3f5cc1f2 100644
--- a/packages/api/src/database/queries/permissions/administrations.test.integration.ts
+++ b/packages/api/src/database/queries/permissions/administrations.test.integration.ts
@@ -33,7 +33,7 @@ describe('administrationsTitresQuery', () => {
       typeId: 'arm',
     } as ITitre
 
-    await Titres.query().insertGraph(mockTitre)
+    await Titres.query().insert(mockTitre)
 
     const q = Titres.query()
       .joinRaw(`left join titres_etapes as t_e on t_e.id = "titres"."props_titre_etapes_ids" ->> 'points' and t_e.administrations_locales @> '"${administrationId}"'::jsonb`)
diff --git a/packages/api/src/database/queries/permissions/titres.test.integration.ts b/packages/api/src/database/queries/permissions/titres.test.integration.ts
index d546909fe..5dd12e39e 100644
--- a/packages/api/src/database/queries/permissions/titres.test.integration.ts
+++ b/packages/api/src/database/queries/permissions/titres.test.integration.ts
@@ -1,4 +1,4 @@
-import { ITitre, ITitreDemarche } from '../../../types'
+import { ITitre, ITitreDemarche, ITitreEtape } from '../../../types'
 
 import { dbManager } from '../../../../tests/db-manager'
 
@@ -15,6 +15,9 @@ import { EtapeStatutId } from 'camino-common/src/static/etapesStatuts'
 import { EtapeTypeId } from 'camino-common/src/static/etapesTypes'
 import { toCaminoDate } from 'camino-common/src/date'
 import { entrepriseIdValidator } from 'camino-common/src/entreprise'
+import TitresDemarches from '../../models/titres-demarches'
+import TitresEtapes from '../../models/titres-etapes'
+import { ETAPE_IS_NOT_BROUILLON } from 'camino-common/src/etape'
 
 console.info = vi.fn()
 console.error = vi.fn()
@@ -26,6 +29,12 @@ afterAll(async () => {
   await dbManager.closeKnex()
 })
 
+const createTitreWithDemarcheWithEtape = async (titre: ITitre, demarche: Omit<ITitreDemarche, 'titreId'>, etape: Omit<ITitreEtape, 'titreDemarcheId'>): Promise<void> => {
+  await Titres.query().insert(titre)
+  await TitresDemarches.query().insert({ ...demarche, titreId: titre.id })
+  await TitresEtapes.query().insert({ ...etape, titreDemarcheId: demarche.id, etapeFondamentaleId: etape.id })
+}
+
 describe('titresQueryModify', () => {
   describe('titresVisibleByEntrepriseQuery', () => {
     test.each([
@@ -37,31 +46,28 @@ describe('titresQueryModify', () => {
       const etapeId = newEtapeId()
 
       const entrepriseId1 = entrepriseIdValidator.parse('entrepriseId1')
-      const mockTitre: Omit<ITitre, 'id'> = {
-        nom: 'titre1',
-        typeId: 'arm',
-        titreStatutId: 'ind',
-        propsTitreEtapesIds: { titulaires: etapeId },
-        demarches: [
-          {
-            id: demarcheId,
-            titreId: id,
-            typeId: 'oct',
-            etapes: [
-              {
-                id: etapeId,
-                titreDemarcheId: demarcheId,
-                date: toCaminoDate('2020-01-01'),
-                typeId: 'mfr',
-                statutId: 'fai',
-                titulaireIds: withTitulaire ? [entrepriseId1] : [],
-              },
-            ],
-          } as ITitreDemarche,
-        ],
-      }
 
-      await Titres.query().insertGraph(mockTitre)
+      await createTitreWithDemarcheWithEtape(
+        {
+          id,
+          nom: 'titre1',
+          typeId: 'arm',
+          titreStatutId: 'ind',
+          propsTitreEtapesIds: { titulaires: etapeId },
+        },
+        {
+          id: demarcheId,
+          typeId: 'oct',
+        },
+        {
+          id: etapeId,
+          date: toCaminoDate('2020-01-01'),
+          typeId: 'mfr',
+          statutId: 'fai',
+          titulaireIds: withTitulaire ? [entrepriseId1] : [],
+          isBrouillon: ETAPE_IS_NOT_BROUILLON,
+        }
+      )
 
       const q = Titres.query()
       titresVisibleByEntrepriseQuery(q, [entrepriseId1])
@@ -80,31 +86,27 @@ describe('titresQueryModify', () => {
       const etapeId = newEtapeId()
 
       const entrepriseId2 = entrepriseIdValidator.parse('entrepriseId2')
-      const mockTitre: Omit<ITitre, 'id'> = {
-        nom: 'titre1',
-        typeId: 'arm',
-        titreStatutId: 'ind',
-        propsTitreEtapesIds: { amodiataires: etapeId },
-        demarches: [
-          {
-            id: demarcheId,
-            titreId: id,
-            typeId: 'oct',
-            etapes: [
-              {
-                id: etapeId,
-                titreDemarcheId: demarcheId,
-                date: toCaminoDate('2020-01-01'),
-                typeId: 'mfr',
-                statutId: 'fai',
-                amodiataireIds: withAmodiataire ? [entrepriseId2] : [],
-              },
-            ],
-          } as ITitreDemarche,
-        ],
-      }
-
-      await Titres.query().insertGraph(mockTitre)
+      await createTitreWithDemarcheWithEtape(
+        {
+          id,
+          nom: 'titre1',
+          typeId: 'arm',
+          titreStatutId: 'ind',
+          propsTitreEtapesIds: { amodiataires: etapeId },
+        },
+        {
+          id: demarcheId,
+          typeId: 'oct',
+        },
+        {
+          id: etapeId,
+          date: toCaminoDate('2020-01-01'),
+          typeId: 'mfr',
+          statutId: 'fai',
+          amodiataireIds: withAmodiataire ? [entrepriseId2] : [],
+          isBrouillon: ETAPE_IS_NOT_BROUILLON,
+        }
+      )
 
       const q = Titres.query()
       titresVisibleByEntrepriseQuery(q, [entrepriseId2])
@@ -126,35 +128,39 @@ describe('titresQueryModify', () => {
       etapeStatutId = 'fav',
     }: {
       visible: boolean
-      titreTypeId?: string
-      titreStatutId?: string
-      demarcheTypeId?: string
-      demarcheStatutId?: string
-      etapeTypeId?: string
-      etapeStatutId?: string
+      titreTypeId?: TitreTypeId
+      titreStatutId?: TitreStatutId
+      demarcheTypeId?: DemarcheTypeId
+      demarcheStatutId?: DemarcheStatutId
+      etapeTypeId?: EtapeTypeId
+      etapeStatutId?: EtapeStatutId
     }) => {
-      const mockTitre = {
-        nom: 'titre1',
-        typeId: titreTypeId,
-        titreStatutId,
-        demarches: [
-          {
-            typeId: demarcheTypeId,
-            statutId: demarcheStatutId,
-            etapes: [
-              {
-                typeId: etapeTypeId,
-                statutId: etapeStatutId,
-                date: '2020-01-01',
-              },
-            ],
-          },
-        ],
-      } as ITitre
-
-      const titre = await Titres.query().insertGraph(mockTitre)
+      const id = newTitreId()
+      const demarcheId = newDemarcheId()
+      const etapeId = newEtapeId()
+      await createTitreWithDemarcheWithEtape(
+        {
+          id,
+          nom: 'titre1',
+          typeId: titreTypeId,
+          titreStatutId,
+          propsTitreEtapesIds: {},
+        },
+        {
+          id: demarcheId,
+          typeId: demarcheTypeId,
+          statutId: demarcheStatutId,
+        },
+        {
+          id: etapeId,
+          typeId: etapeTypeId,
+          statutId: etapeStatutId,
+          date: toCaminoDate('2020-01-01'),
+          isBrouillon: ETAPE_IS_NOT_BROUILLON,
+        }
+      )
 
-      const res = await Titres.query().where('id', titre.id).modify(titresArmEnDemandeQuery)
+      const res = await Titres.query().where('id', id).modify(titresArmEnDemandeQuery)
 
       expect(res).toHaveLength(visible ? 1 : 0)
     }
@@ -215,38 +221,31 @@ describe('titresQueryModify', () => {
       const id = newTitreId()
 
       const entrepriseId1 = entrepriseIdValidator.parse('entrepriseId1')
-      const mockTitre: ITitre = {
-        id,
-        nom: 'titre1',
-        typeId,
-        titreStatutId: statutId,
-        publicLecture,
-        propsTitreEtapesIds: { titulaires: etapeId },
-        demarches: [
-          {
-            id: demarcheId,
-            titreId: id,
-            typeId: 'oct',
-            statutId: 'ins',
-            etapes: [
-              {
-                id: etapeId,
-                titreDemarcheId: demarcheId,
-                date: toCaminoDate('2020-01-01'),
-                typeId: 'mcr',
-                statutId: 'fav',
-                titulaireIds: withTitulaire ? [entrepriseId1] : [],
-              },
-            ],
-          } as ITitreDemarche,
-        ],
-      }
-
-      await Titres.query().insertGraph(mockTitre)
-
-      const q = Titres.query().where('id', id).modify(titresConfidentielSelect, [entrepriseId1]) // eslint-disable-line @typescript-eslint/no-misused-promises
-
-      const res = await q
+      await createTitreWithDemarcheWithEtape(
+        {
+          id,
+          nom: 'titre1',
+          typeId,
+          titreStatutId: statutId,
+          publicLecture,
+          propsTitreEtapesIds: { titulaires: etapeId },
+        },
+        {
+          id: demarcheId,
+          typeId: 'oct',
+          statutId: 'ins',
+        },
+        {
+          id: etapeId,
+          date: toCaminoDate('2020-01-01'),
+          typeId: 'mcr',
+          statutId: 'fav',
+          titulaireIds: withTitulaire ? [entrepriseId1] : [],
+          isBrouillon: ETAPE_IS_NOT_BROUILLON,
+        }
+      )
+
+      const res = await Titres.query().where('id', id).modify(titresConfidentielSelect, [entrepriseId1]) // eslint-disable-line @typescript-eslint/no-misused-promises
 
       expect(res).toHaveLength(1)
       if (confidentiel) {
diff --git a/packages/api/src/database/queries/titres-activites.test.integration.ts b/packages/api/src/database/queries/titres-activites.test.integration.ts
index 2357b3111..22765f41b 100644
--- a/packages/api/src/database/queries/titres-activites.test.integration.ts
+++ b/packages/api/src/database/queries/titres-activites.test.integration.ts
@@ -30,7 +30,7 @@ describe('teste les requêtes sur les activités', () => {
     })
 
     const titreActiviteId = activiteIdValidator.parse('titreActiviteId')
-    await TitresActivites.query().insertGraph({
+    await TitresActivites.query().insert({
       id: titreActiviteId,
       typeId: 'grx',
       titreId,
diff --git a/packages/api/src/database/queries/titres-activites.ts b/packages/api/src/database/queries/titres-activites.ts
index 4718a423d..7e34cb3b3 100644
--- a/packages/api/src/database/queries/titres-activites.ts
+++ b/packages/api/src/database/queries/titres-activites.ts
@@ -101,7 +101,7 @@ const titreActivitesQueryBuild = ({ fields }: { fields?: FieldsActivite }, user:
   return q
 }
 
-const titreActiviteGet = async (id: string, { fields }: { fields?: FieldsActivite }, user: User) => {
+export const titreActiviteGet = async (id: string, { fields }: { fields?: FieldsActivite }, user: User): Promise<ITitreActivite | undefined> => {
   const q = titreActivitesQueryBuild({ fields }, user)
 
   return q
@@ -152,7 +152,7 @@ const titresActivitesColonnes = {
  *
  */
 
-const titresActivitesGet = async (
+export const titresActivitesGet = async (
   {
     page,
     intervalle,
@@ -188,7 +188,7 @@ const titresActivitesGet = async (
   },
   { fields }: { fields?: FieldsActivite },
   user: User
-) => {
+): Promise<ITitreActivite[]> => {
   const q = titreActivitesQueryBuild({ fields }, user)
 
   titresActivitesFiltersQueryModify(
@@ -249,7 +249,7 @@ const titresActivitesGet = async (
  *
  */
 
-const titresActivitesCount = async (
+export const titresActivitesCount = async (
   {
     typesIds,
     statutsIds,
@@ -275,7 +275,7 @@ const titresActivitesCount = async (
   },
   { fields }: { fields?: FieldsActivite },
   user: User
-) => {
+): Promise<number> => {
   const q = titreActivitesQueryBuild({ fields }, user)
 
   titresActivitesFiltersQueryModify(
@@ -297,9 +297,6 @@ const titresActivitesCount = async (
   return q.resultSize()
 }
 
-const titresActivitesUpsert = async (titreActivites: ITitreActivite[]) =>
-  TitresActivites.query().withGraphFetched(options.titresActivites.graph).upsertGraph(titreActivites, options.titresActivites.update)
-
-const titreActiviteUpdate = async (id: ActiviteId, titreActivite: Partial<ITitreActivite>) => TitresActivites.query().patchAndFetchById(id, { ...titreActivite, id })
+export const titresActivitesInsert = async (titreActivites: Omit<ITitreActivite, 'titre'>[]): Promise<ITitreActivite[]> => TitresActivites.query().insert(titreActivites)
 
-export { titreActiviteGet, titresActivitesCount, titresActivitesUpsert, titresActivitesGet, titreActiviteUpdate }
+export const titreActiviteUpdate = async (_id: ActiviteId, titreActivite: Partial<ITitreActivite>): Promise<number> => TitresActivites.query().patch(titreActivite)
diff --git a/packages/api/src/database/queries/titres-etapes.queries.ts b/packages/api/src/database/queries/titres-etapes.queries.ts
index 95458d4aa..ac099128f 100644
--- a/packages/api/src/database/queries/titres-etapes.queries.ts
+++ b/packages/api/src/database/queries/titres-etapes.queries.ts
@@ -1,5 +1,5 @@
 import { sql } from '@pgtyped/runtime'
-import { dbQueryAndValidate, Redefine } from '../../pg-database'
+import { dbQueryAndValidate, effectDbQueryAndValidate, EffectDbQueryAndValidateErrors, Redefine } from '../../pg-database'
 import {
   IDeleteTitreEtapeEntrepriseDocumentInternalQuery,
   IGetEntrepriseDocumentIdsByEtapeIdQueryQuery,
@@ -18,6 +18,7 @@ import {
   IDeleteEtapeAvisDbQuery,
   IGetEtapesWithAutomaticStatutDbQuery,
   IUpdateEtapeStatutDbQuery,
+  IUpdateEtapeFondamentaleIdDbQuery,
 } from './titres-etapes.queries.types'
 import {
   ETAPE_IS_NOT_BROUILLON,
@@ -61,6 +62,8 @@ import { CommuneId } from 'camino-common/src/static/communes'
 import { EtapeStatutId, etapeStatutIdValidator } from 'camino-common/src/static/etapesStatuts'
 import { contenuValidator, heritageContenuValidator } from 'camino-common/src/etape-form'
 import { demarcheTypeIdValidator } from 'camino-common/src/static/demarchesTypes'
+import { Effect } from 'effect'
+import { CaminoError } from 'camino-common/src/zod-tools'
 
 export const insertTitreEtapeEntrepriseDocument = async (pool: Pool, params: { titre_etape_id: EtapeId; entreprise_document_id: EntrepriseDocumentId }): Promise<void[]> =>
   dbQueryAndValidate(insertTitreEtapeEntrepriseDocumentInternal, params, pool, z.void())
@@ -457,3 +460,10 @@ export const updateEtapeStatut = async (pool: Pool, etapeId: EtapeId, newStatut:
 const updateEtapeStatutDb = sql<Redefine<IUpdateEtapeStatutDbQuery, { newStatut: EtapeStatutId; etapeId: EtapeId }, void>>`
   UPDATE titres_etapes SET statut_id = $ newStatut ! where id = $ etapeId !
 `
+
+export const updateEtapeFondamentaleId = (pool: Pool, etapeId: EtapeId, etapeFondamentaleId: EtapeId): Effect.Effect<true, CaminoError<EffectDbQueryAndValidateErrors>> =>
+  effectDbQueryAndValidate(updateEtapeFondamentaleIdDb, { etapeId, etapeFondamentaleId }, pool, z.void()).pipe(Effect.map(() => true))
+
+const updateEtapeFondamentaleIdDb = sql<Redefine<IUpdateEtapeFondamentaleIdDbQuery, { etapeFondamentaleId: EtapeId; etapeId: EtapeId }, void>>`
+  UPDATE titres_etapes SET etape_fondamentale_id = $ etapeFondamentaleId ! where id = $ etapeId !
+`
diff --git a/packages/api/src/database/queries/titres-etapes.queries.types.ts b/packages/api/src/database/queries/titres-etapes.queries.types.ts
index 39cc32437..80db6b69d 100644
--- a/packages/api/src/database/queries/titres-etapes.queries.types.ts
+++ b/packages/api/src/database/queries/titres-etapes.queries.types.ts
@@ -305,3 +305,18 @@ export interface IUpdateEtapeStatutDbQuery {
   result: IUpdateEtapeStatutDbResult;
 }
 
+/** 'UpdateEtapeFondamentaleIdDb' parameters type */
+export interface IUpdateEtapeFondamentaleIdDbParams {
+  etapeFondamentaleId: string;
+  etapeId: string;
+}
+
+/** 'UpdateEtapeFondamentaleIdDb' return type */
+export type IUpdateEtapeFondamentaleIdDbResult = void;
+
+/** 'UpdateEtapeFondamentaleIdDb' query type */
+export interface IUpdateEtapeFondamentaleIdDbQuery {
+  params: IUpdateEtapeFondamentaleIdDbParams;
+  result: IUpdateEtapeFondamentaleIdDbResult;
+}
+
diff --git a/packages/api/src/database/queries/titres-etapes.ts b/packages/api/src/database/queries/titres-etapes.ts
index 3658052bd..64db9e46c 100644
--- a/packages/api/src/database/queries/titres-etapes.ts
+++ b/packages/api/src/database/queries/titres-etapes.ts
@@ -1,5 +1,5 @@
 import { ITitreEtape } from '../../types'
-import options, { FieldsEtape } from './_options'
+import { FieldsEtape } from './_options'
 import graphBuild from './graph/build'
 import { fieldsFormat } from './graph/fields-format'
 
@@ -81,4 +81,4 @@ export const titreEtapeUpdate = async (id: EtapeId, titreEtape: Partial<DBTitres
 }
 
 export const titreEtapeUpsert = async (titreEtape: Partial<Pick<ITitreEtape, 'id'>> & Omit<ITitreEtape, 'id'>, user: DeepReadonly<UserNotNull>, titreId: TitreId): Promise<ITitreEtape | undefined> =>
-  upsertJournalCreate(titreEtape.id, titreEtape, options.titresEtapes.update, '[]', user.id, titreId)
+  upsertJournalCreate(titreEtape.id, titreEtape, user.id, titreId)
diff --git a/packages/api/src/database/queries/titres.ts b/packages/api/src/database/queries/titres.ts
index 584c18dd8..08ac73f54 100644
--- a/packages/api/src/database/queries/titres.ts
+++ b/packages/api/src/database/queries/titres.ts
@@ -18,7 +18,6 @@ import { User } from 'camino-common/src/roles'
 import { DepartementId } from 'camino-common/src/static/departement'
 import { RegionId } from 'camino-common/src/static/region'
 import { FacadesMaritimes } from 'camino-common/src/static/facades'
-import { EditableTitre } from 'camino-common/src/titres'
 import { TitreId } from 'camino-common/src/validators/titres'
 import { DeepReadonly } from 'camino-common/src/typescript-tools'
 
@@ -211,7 +210,7 @@ export const titresCount = async (
   } = {},
   { fields }: { fields?: FieldsTitre },
   user: User
-) => {
+): Promise<number> => {
   const q = titresQueryBuild({ fields }, user, demandeEnCours)
 
   titresFiltersQueryModify(
@@ -235,24 +234,9 @@ export const titresCount = async (
   return q.resultSize()
 }
 
-/**
- * Crée un nouveau titre
- *
- * @param titre - titre à créer
- * @param fields - Non utilisés
- * @param userId - id de l’utilisateur
- * @returns le nouveau titre
- *
- */
-export const titreCreate = async (titre: Omit<ITitre, 'id'>, { fields }: { fields?: FieldsTitre }): Promise<DBTitre> => {
-  const graph = fields ? graphBuild(titresFieldsAdd(fields), 'titre', fieldsFormat) : options.titres.graph
+export const titreUpdate = async (id: TitreId, titre: Partial<DBTitre>): Promise<DBTitre> => Titres.query().patchAndFetchById(id, { ...titre, id })
 
-  return Titres.query().withGraphFetched(graph).insertGraph(titre, options.titres.update)
-}
-
-export const titreUpdate = async (id: TitreId, titre: Partial<DBTitre>) => Titres.query().patchAndFetchById(id, { ...titre, id })
-
-export const titreArchive = async (id: TitreId) => {
+export const titreArchive = async (id: TitreId): Promise<void> => {
   // archive le titre
   await titreUpdate(id, { archive: true })
 
@@ -262,7 +246,3 @@ export const titreArchive = async (id: TitreId) => {
   // archive les étapes des démarches du titre
   await TitresEtapes.query().patch({ archive: true }).whereIn('titreDemarcheId', TitresDemarches.query().select('id').where('titreId', id))
 }
-
-export const titreUpsert = async (titre: EditableTitre) => {
-  return Titres.query().upsertGraph(titre, options.titres.update)
-}
diff --git a/packages/api/src/knex/migrations/20240925132503_add-etape-fondamentale-fk.ts b/packages/api/src/knex/migrations/20240925132503_add-etape-fondamentale-fk.ts
new file mode 100644
index 000000000..5b52c339d
--- /dev/null
+++ b/packages/api/src/knex/migrations/20240925132503_add-etape-fondamentale-fk.ts
@@ -0,0 +1,39 @@
+import { DemarcheId, demarcheIdValidator } from 'camino-common/src/demarche'
+import { EtapeId } from 'camino-common/src/etape'
+import { EtapesTypes, EtapeTypeId } from 'camino-common/src/static/etapesTypes'
+import { getKeys, isNullOrUndefined } from 'camino-common/src/typescript-tools'
+import { Knex } from 'knex'
+type MyEtape = { id: EtapeId; type_id: EtapeTypeId; titre_demarche_id: DemarcheId; ordre: number }
+export const up = async (knex: Knex): Promise<void> => {
+  await knex.raw('ALTER TABLE titres_etapes ADD etape_fondamentale_id varchar NULL')
+  await knex.raw('ALTER TABLE titres_etapes ADD CONSTRAINT etape_fondamentale_fk FOREIGN KEY (etape_fondamentale_id) REFERENCES public.titres_etapes(id)')
+
+  await knex.raw('UPDATE titres_etapes set etape_fondamentale_id=id where archive is true')
+
+  const { rows: etapes }: { rows: MyEtape[] } = await knex.raw('SELECT id, titre_demarche_id, ordre, type_id FROM titres_etapes WHERE archive is false order by titre_demarche_id, ordre')
+
+  const etapesByDemarcheId = etapes.reduce<{ [key in DemarcheId]?: MyEtape[] }>((acc, etape) => {
+    if (isNullOrUndefined(acc[etape.titre_demarche_id])) {
+      acc[etape.titre_demarche_id] = []
+    }
+    acc[etape.titre_demarche_id]?.push(etape)
+    return acc
+  }, {})
+
+  for (const demarche of getKeys(etapesByDemarcheId, (value): value is DemarcheId => demarcheIdValidator.safeParse(value).success)) {
+    const etapesByDemarche = etapesByDemarcheId[demarche] ?? []
+    if (etapesByDemarche.length > 0) {
+      let etapeFondamentaleId = etapesByDemarche[0].id
+      for (const currentEtape of etapesByDemarche) {
+        if (EtapesTypes[currentEtape.type_id].fondamentale) {
+          etapeFondamentaleId = currentEtape.id
+        }
+        await knex.raw('UPDATE titres_etapes set etape_fondamentale_id=? where id=?', [etapeFondamentaleId, currentEtape.id])
+      }
+    }
+  }
+
+  await knex.raw('ALTER TABLE titres_etapes ALTER COLUMN etape_fondamentale_id SET NOT NULL;')
+}
+
+export const down = (): void => {}
diff --git a/packages/api/src/pg-database.ts b/packages/api/src/pg-database.ts
index 06dba2024..9b8ebc8de 100644
--- a/packages/api/src/pg-database.ts
+++ b/packages/api/src/pg-database.ts
@@ -30,13 +30,13 @@ export const dbQueryAndValidate = async <Params, Result, T extends ZodType<Resul
 }
 
 export type DbQueryAccessError = "Impossible d'accéder à la base de données"
-
+export type EffectDbQueryAndValidateErrors = DbQueryAccessError | ZodUnparseable
 export const effectDbQueryAndValidate = <Params, Result, T extends ZodType<Result, ZodTypeDef, unknown>>(
   query: TaggedQuery<{ params: DeepReadonly<Params>; result: Result }>,
   params: DeepReadonly<Params>,
   pool: Pool,
   validator: T
-): Effect.Effect<Result[], CaminoError<DbQueryAccessError | ZodUnparseable>> => {
+): Effect.Effect<Result[], CaminoError<EffectDbQueryAndValidateErrors>> => {
   return pipe(
     Effect.tryPromise({
       try: () => query.run(params, pool),
diff --git a/packages/api/src/tools/demarches/definitions-check.ts b/packages/api/src/tools/demarches/definitions-check.ts
index 663d403ce..64d6d2330 100644
--- a/packages/api/src/tools/demarches/definitions-check.ts
+++ b/packages/api/src/tools/demarches/definitions-check.ts
@@ -4,8 +4,10 @@ import { titreDemarcheUpdatedEtatValidate } from '../../business/validations/tit
 import { userSuper } from '../../database/user-super'
 import { getTitreTypeType, getDomaineId } from 'camino-common/src/static/titresTypes'
 import { isNotNullNorUndefinedNorEmpty, onlyUnique } from 'camino-common/src/typescript-tools'
+import { DemarcheId } from 'camino-common/src/demarche'
 
 const demarchesValidate = async () => {
+  const demarchesIdsAlreadyChecked: Set<DemarcheId> = new Set()
   const errorsTotal = [] as string[]
   for (const demarcheDefinition of demarchesDefinitions) {
     for (const demarcheTypeId of demarcheDefinition.demarcheTypeIds) {
@@ -17,22 +19,27 @@ const demarchesValidate = async () => {
         },
         {
           fields: {
-            titre: { id: {}, demarches: { etapes: { id: {} } } },
+            titre: { id: {}, demarches: { id: {} } },
             etapes: { id: {} },
           },
         },
         userSuper
       )
       for (const demarche of demarches) {
-        if (isNotNullNorUndefinedNorEmpty(demarche.etapes)) {
-          try {
-            const { valid, errors } = titreDemarcheUpdatedEtatValidate(demarche.typeId, demarche.titre!, demarche.etapes![0], demarche.id, demarche.etapes!)
+        if (!demarchesIdsAlreadyChecked.has(demarche.id)) {
+          demarchesIdsAlreadyChecked.add(demarche.id)
 
-            if (!valid) {
-              errorsTotal.push(`https://camino.beta.gouv.fr/demarches/${demarche.slug} => démarche "${demarche.typeId}" : ${errors}`)
+          if (isNotNullNorUndefinedNorEmpty(demarche.etapes)) {
+            try {
+              const sortedEtapes = demarche.etapes!.toSorted((a, b) => (a.ordre ?? 0) - (b.ordre ?? 0))
+              const { valid, errors } = titreDemarcheUpdatedEtatValidate(demarche.typeId, demarche.titre!, sortedEtapes![0], demarche.id, sortedEtapes!)
+
+              if (!valid) {
+                errorsTotal.push(`https://camino.beta.gouv.fr/demarches/${demarche.slug} => démarche "${demarche.typeId}" : ${errors}`)
+              }
+            } catch (e) {
+              errorsTotal.push(`${demarche.id} démarche invalide =>\n\t${e}`)
             }
-          } catch (e) {
-            errorsTotal.push(`${demarche.id} démarche invalide =>\n\t${e}`)
           }
         }
       }
diff --git a/packages/api/src/tools/fp-tools.ts b/packages/api/src/tools/fp-tools.ts
index bbc13ce37..de51dd16f 100644
--- a/packages/api/src/tools/fp-tools.ts
+++ b/packages/api/src/tools/fp-tools.ts
@@ -5,6 +5,8 @@ import { fromError, isZodErrorLike } from 'zod-validation-error'
 
 export type ZodUnparseable = 'Problème de validation de données'
 
+export const shortCircuitError = <T extends string>(value: T): { _tag: T } => ({ _tag: value })
+
 export const zodParseEffectCallback =
   <T extends ZodTypeAny>(validator: T) =>
   (value: unknown): Effect.Effect<T['_output'], CaminoError<ZodUnparseable>> =>
diff --git a/packages/api/tests/_utils/administrations-permissions.ts b/packages/api/tests/_utils/administrations-permissions.ts
index 996f8de60..bb07259d7 100644
--- a/packages/api/tests/_utils/administrations-permissions.ts
+++ b/packages/api/tests/_utils/administrations-permissions.ts
@@ -26,7 +26,7 @@ import { userSuper } from '../../src/database/user-super'
 import { defaultHeritageProps } from 'camino-common/src/etape-form'
 import { exhaustiveCheck, isNotNullNorUndefined, isNullOrUndefined } from 'camino-common/src/typescript-tools'
 import { HTTP_STATUS } from 'camino-common/src/http'
-import { titreCreate } from '../../src/database/queries/titres'
+import { insertTitreGraph } from '../integration-test-helper'
 
 const dir = `${process.cwd()}/files/tmp/`
 
@@ -281,9 +281,10 @@ export const creationCheck = async (pool: Pool, administrationId: string, creer:
 }
 
 const titreCreateSuper = async (_pool: Pool, administrationId: string, titreTypeId: TitreTypeId) => {
-  const titre = await titreCreate({ nom: `titre-${titreTypeId!}-cree-${administrationId!}`, typeId: titreTypeId, titreStatutId: 'ind', propsTitreEtapesIds: {} }, {})
+  const titreId = newTitreId()
+  await insertTitreGraph({ id: titreId, nom: `titre-${titreTypeId!}-cree-${administrationId!}`, typeId: titreTypeId, titreStatutId: 'ind', propsTitreEtapesIds: {} })
 
-  return titre.id
+  return titreId
 }
 
 const demarcheCreerProfil = async (pool: Pool, titreId: TitreId, user: TestUser) => restNewPostCall(pool, '/rest/demarches', {}, user, { titreId, typeId: 'oct', description: '' })
diff --git a/packages/api/tests/integration-test-helper.ts b/packages/api/tests/integration-test-helper.ts
new file mode 100644
index 000000000..a7956ac0c
--- /dev/null
+++ b/packages/api/tests/integration-test-helper.ts
@@ -0,0 +1,18 @@
+import Titres from '../src/database/models/titres'
+import TitresDemarches from '../src/database/models/titres-demarches'
+import TitresEtapes from '../src/database/models/titres-etapes'
+import { ITitre } from '../src/types'
+
+export const insertTitreGraph = async (titreGraph: ITitre): Promise<void> => {
+  const { demarches, ...titre } = titreGraph
+  await Titres.query().insert(titre)
+
+  for (const demarcheGraph of demarches ?? []) {
+    const { etapes, ...demarche } = demarcheGraph
+    await TitresDemarches.query().insert(demarche)
+
+    for (const etape of etapes ?? []) {
+      await TitresEtapes.query().insert({ etapeFondamentaleId: etape.id, ...etape })
+    }
+  }
+}
diff --git a/packages/common/src/date.ts b/packages/common/src/date.ts
index c134dcbc4..30ed10121 100644
--- a/packages/common/src/date.ts
+++ b/packages/common/src/date.ts
@@ -15,6 +15,7 @@ export const daysBetween = (a: CaminoDate, b: CaminoDate): number => {
 export const isBefore = (a: CaminoDate, b: CaminoDate): boolean => {
   return a < b
 }
+
 export const caminoDateValidator = z
   .string()
   .regex(/^\d{4}-\d{2}-\d{2}$/)
diff --git a/packages/common/src/perimetre.ts b/packages/common/src/perimetre.ts
index 05fb958e9..18492f1d2 100644
--- a/packages/common/src/perimetre.ts
+++ b/packages/common/src/perimetre.ts
@@ -147,20 +147,13 @@ export const geojsonImportForagesResponseValidator = z.object({ geojson4326: fea
 export type GeojsonImportForagesResponse = z.infer<typeof geojsonImportForagesResponseValidator>
 
 const internalEqualGeojson = (geo1: MultiPolygon, geo2: MultiPolygon): boolean => {
-  for (let indexLevel1 = 0; indexLevel1 < geo1.coordinates.length; indexLevel1++) {
-    for (let indexLevel2 = 0; indexLevel2 < geo1.coordinates[indexLevel1].length; indexLevel2++) {
-      for (let indexLevel3 = 0; indexLevel3 < geo1.coordinates[indexLevel1][indexLevel2].length; indexLevel3++) {
-        if (
-          geo1.coordinates[indexLevel1][indexLevel2][indexLevel3][0] !== geo2.coordinates[indexLevel1]?.[indexLevel2]?.[indexLevel3]?.[0] ||
-          geo1.coordinates[indexLevel1][indexLevel2][indexLevel3][1] !== geo2.coordinates[indexLevel1]?.[indexLevel2]?.[indexLevel3]?.[1]
-        ) {
-          return false
-        }
-      }
-    }
-  }
-
-  return true
+  return geo1.coordinates.every((level1, indexLevel1) => {
+    return level1.every((level2, indexLevel2) => {
+      return level2.every((level3, indexLevel3) => {
+        return level3[0] === geo2.coordinates[indexLevel1]?.[indexLevel2]?.[indexLevel3]?.[0] && level3[1] === geo2.coordinates[indexLevel1]?.[indexLevel2]?.[indexLevel3]?.[1]
+      })
+    })
+  })
 }
 
 export const equalGeojson = (geo1: MultiPolygon, geo2: MultiPolygon | null | undefined): boolean => {
diff --git a/packages/common/src/permissions/journaux.test.ts b/packages/common/src/permissions/journaux.test.ts
new file mode 100644
index 000000000..9f76ff10e
--- /dev/null
+++ b/packages/common/src/permissions/journaux.test.ts
@@ -0,0 +1,7 @@
+import { test, expect } from 'vitest'
+import { testBlankUser } from '../tests-utils'
+import { canReadJournaux } from './journaux'
+
+test('canReadJournaux', () => {
+  expect(canReadJournaux({ ...testBlankUser, role: 'super' })).toBe(true)
+})
diff --git a/packages/common/src/permissions/titres-demarches.ts b/packages/common/src/permissions/titres-demarches.ts
index eebff926e..ab0da178e 100644
--- a/packages/common/src/permissions/titres-demarches.ts
+++ b/packages/common/src/permissions/titres-demarches.ts
@@ -8,12 +8,12 @@ import { getEtapesTDE } from '../static/titresTypes_demarchesTypes_etapesTypes/i
 import { DemarcheTypeId } from '../static/demarchesTypes'
 import { canCreateEtape } from './titres-etapes'
 import { TitreGetDemarche } from '../titres'
-import { DeepReadonly, isNullOrUndefined } from '../typescript-tools'
+import { DeepReadonly, isNotNullNorUndefinedNorEmpty, isNullOrUndefined } from '../typescript-tools'
 import { ETAPE_IS_BROUILLON } from '../etape'
 
 const hasOneDemarcheWithoutPhase = (demarches: Pick<TitreGetDemarche, 'demarche_date_debut'>[]): boolean => {
   // Si il y a une seule démarche et qu’elle n’a pas encore créée de phase, alors on ne peut pas créer une deuxième démarche
-  return demarches.length === 1 && isNullOrUndefined(demarches[0].demarche_date_debut)
+  return isNotNullNorUndefinedNorEmpty(demarches) && demarches.length === 1 && isNullOrUndefined(demarches[0].demarche_date_debut)
 }
 export const canCreateDemarche = (
   user: DeepReadonly<User>,
diff --git a/packages/common/src/permissions/titres-etapes.test.ts b/packages/common/src/permissions/titres-etapes.test.ts
index f9fc01620..a07acc5cc 100644
--- a/packages/common/src/permissions/titres-etapes.test.ts
+++ b/packages/common/src/permissions/titres-etapes.test.ts
@@ -15,9 +15,10 @@ import {
   IsEtapeCompleteEntrepriseDocuments,
   IsEtapeCompleteEtape,
   canDeleteEtape,
+  isEtapeDeposable,
 } from './titres-etapes'
 import { AdministrationId, ADMINISTRATION_IDS } from '../static/administrations'
-import { test, expect } from 'vitest'
+import { test, expect, describe } from 'vitest'
 import { TestUser, testBlankUser } from '../tests-utils'
 import { TitreStatutId } from '../static/titresStatuts'
 import { EntrepriseId, entrepriseIdValidator, newEntrepriseId } from '../entreprise'
@@ -60,6 +61,8 @@ test.each<{ titreTypeId: TitreTypeId; demarcheTypeId: DemarcheTypeId; etapeTypeI
   { titreTypeId: 'arm', etapeTypeId: 'mfr', demarcheTypeId: 'dec', user: { role: 'super' }, canEdit: false },
   { titreTypeId: 'arm', etapeTypeId: 'dpu', demarcheTypeId: 'dec', user: { role: 'super' }, canEdit: true },
   { titreTypeId: 'axm', etapeTypeId: 'mfr', demarcheTypeId: 'dec', user: { role: 'super' }, canEdit: false },
+  { titreTypeId: 'axm', etapeTypeId: 'dpu', demarcheTypeId: 'dec', user: { role: 'admin', administrationId: ADMINISTRATION_IDS['DGTM - GUYANE'] }, canEdit: true },
+  { titreTypeId: 'axm', etapeTypeId: 'dpu', demarcheTypeId: 'dec', user: { role: 'editeur', administrationId: ADMINISTRATION_IDS['DGTM - GUYANE'] }, canEdit: true },
   { titreTypeId: 'prm', etapeTypeId: 'mfr', demarcheTypeId: 'dec', user: { role: 'super' }, canEdit: true },
   { titreTypeId: 'prm', etapeTypeId: 'mfr', demarcheTypeId: 'dec', user: { role: 'admin', administrationId: ADMINISTRATION_IDS.BRGM }, canEdit: true },
   { titreTypeId: 'prm', etapeTypeId: 'mfr', demarcheTypeId: 'dec', user: { role: 'lecteur', administrationId: ADMINISTRATION_IDS.BRGM }, canEdit: true },
@@ -500,7 +503,7 @@ test('une demande d’ARM mécanisée a des documents obligatoires supplémentai
     'oct',
     armDocuments,
     armEntrepriseDocuments,
-    [],
+    null,
     [],
     null,
     null,
@@ -547,3 +550,47 @@ test.each<[number | null, EtapeTypeId, TitreTypeId, IsEtapeCompleteDocuments, Is
     expect(result).toStrictEqual({ valid: true })
   }
 })
+
+describe('isEtapeDeposable', () => {
+  test('Une demande d’ARM complète brouillon', () => {
+    expect(
+      isEtapeDeposable(
+        { ...testBlankUser, role: 'super' },
+        'arm',
+        'oct',
+        { ...etapeComplete, isBrouillon: ETAPE_IS_BROUILLON, contenu: { arm: { mecanise: { value: false, etapeHeritee: null, heritee: false } } } },
+        armDocuments,
+        armEntrepriseDocuments,
+        [],
+        [],
+        null,
+        null,
+        []
+      )
+    ).toStrictEqual(true)
+  })
+
+  test('Une demande d’ARM complète déjà déposée', () => {
+    expect(
+      isEtapeDeposable(
+        { ...testBlankUser, role: 'super' },
+        'arm',
+        'oct',
+        { ...etapeComplete, isBrouillon: ETAPE_IS_NOT_BROUILLON, contenu: { arm: { mecanise: { value: false, etapeHeritee: null, heritee: false } } } },
+        armDocuments,
+        armEntrepriseDocuments,
+        [],
+        [],
+        null,
+        null,
+        []
+      )
+    ).toStrictEqual(false)
+  })
+
+  test('Une demande d’ARM incomplète', () => {
+    expect(
+      isEtapeDeposable({ ...testBlankUser, role: 'super' }, 'arm', 'oct', { ...etapeComplete, isBrouillon: ETAPE_IS_BROUILLON }, armDocuments, armEntrepriseDocuments, [], [], null, null, [])
+    ).toStrictEqual(false)
+  })
+})
diff --git a/packages/common/src/static/titresTypes_demarchesTypes_etapesTypes/documents.test.ts b/packages/common/src/static/titresTypes_demarchesTypes_etapesTypes/documents.test.ts
index 7efd66818..3155c4c62 100644
--- a/packages/common/src/static/titresTypes_demarchesTypes_etapesTypes/documents.test.ts
+++ b/packages/common/src/static/titresTypes_demarchesTypes_etapesTypes/documents.test.ts
@@ -1,7 +1,11 @@
 import { ETAPES_TYPES } from '../etapesTypes'
-import { getDocuments } from './documents'
+import { getDocuments, toDocuments } from './documents'
 import { test, expect } from 'vitest'
 
+test('toDocuments', () => {
+  expect(toDocuments()).toHaveLength(222)
+})
+
 test('getDocuments erreurs', () => {
   expect(() => getDocuments()).toThrowErrorMatchingInlineSnapshot(`[Error: il manque des éléments pour trouver les documents titreTypeId: 'undefined', demarcheId: undefined, etapeTypeId: undefined]`)
 })
diff --git a/packages/common/src/static/titresTypes_demarchesTypes_etapesTypes/documents.ts b/packages/common/src/static/titresTypes_demarchesTypes_etapesTypes/documents.ts
index eae91df62..5c8c5b6c6 100644
--- a/packages/common/src/static/titresTypes_demarchesTypes_etapesTypes/documents.ts
+++ b/packages/common/src/static/titresTypes_demarchesTypes_etapesTypes/documents.ts
@@ -1,7 +1,7 @@
-import { isNotNullNorUndefined } from '../../typescript-tools'
+import { getEntriesHardcore, isNotNullNorUndefined } from '../../typescript-tools'
 import { DEMARCHES_TYPES_IDS, DemarcheTypeId } from './../demarchesTypes'
 import { DocumentsTypes, DOCUMENTS_TYPES_IDS, DocumentType, DocumentTypeId, isDocumentTypeId } from './../documentsTypes'
-import { ETAPES_TYPES, EtapeTypeId, isEtapeTypeId } from './../etapesTypes'
+import { ETAPES_TYPES, EtapeTypeId } from './../etapesTypes'
 import { TitreTypeId, TITRES_TYPES_IDS } from './../titresTypes'
 import { TDEType } from './index'
 
@@ -390,13 +390,9 @@ const TDEDocumentsTypes = {
 type TDEDocumentsTypesUnleashed = { [key in TitreTypeId]?: { [key in DemarcheTypeId]?: { [key in EtapeTypeId]?: { [key in DocumentTypeId]: { optionnel: boolean; description?: string } } } } }
 
 export const toDocuments = (): { etapeTypeId: EtapeTypeId; documentTypeId: DocumentTypeId; optionnel: boolean; description: string | null }[] => {
-  return Object.entries(EtapesTypesDocumentsTypes).flatMap(([key, values]) => {
-    if (isEtapeTypeId(key)) {
-      return values.map(value => ({ etapeTypeId: key, documentTypeId: value.documentTypeId, description: null, optionnel: value.optionnel }))
-    } else {
-      return []
-    }
-  })
+  return getEntriesHardcore(EtapesTypesDocumentsTypes).flatMap(([key, values]) =>
+    values.map(value => ({ etapeTypeId: key, documentTypeId: value.documentTypeId, description: null, optionnel: value.optionnel }))
+  )
 }
 
 export const getDocuments = (titreTypeId?: TitreTypeId, demarcheId?: DemarcheTypeId, etapeTypeId?: EtapeTypeId): DocumentType[] => {
diff --git a/packages/common/src/strings.test.ts b/packages/common/src/strings.test.ts
index 49cb37a70..c4fd6d1e9 100644
--- a/packages/common/src/strings.test.ts
+++ b/packages/common/src/strings.test.ts
@@ -14,6 +14,8 @@ test('levenshtein', () => {
   expect(levenshtein('or', 'or')).toBe(0)
   expect(levenshtein('oru', 'or')).toBe(1)
   expect(levenshtein('port', 'or')).toBe(2)
+  expect(levenshtein('or', 'port')).toBe(2)
+  expect(levenshtein('', 'port')).toBe(4)
 })
 
 test('slugify', () => {
diff --git a/packages/common/src/typescript-tools.ts b/packages/common/src/typescript-tools.ts
index c0218474f..2f84e92f6 100644
--- a/packages/common/src/typescript-tools.ts
+++ b/packages/common/src/typescript-tools.ts
@@ -5,6 +5,12 @@ export function isNotNullNorUndefined<T>(value: T | null | undefined): value is
   return !isNullOrUndefined(value)
 }
 
+export function toSorted<U>(value: NonEmptyArray<U>, comparator: (a: U, b: U) => number): NonEmptyArray<U>
+export function toSorted<U>(value: U[], comparator: (a: U, b: U) => number): U[]
+export function toSorted<U>(value: U[], comparator: (a: U, b: U) => number): U[] {
+  return [...value].sort(comparator)
+}
+
 export function isNotNullNorUndefinedNorEmpty<U>(value: DeepReadonly<U[]> | null | undefined): value is DeepReadonly<NonEmptyArray<U>>
 export function isNotNullNorUndefinedNorEmpty<U>(value: U[] | null | undefined): value is NonEmptyArray<U>
 export function isNotNullNorUndefinedNorEmpty(value: string | null | undefined): value is string
@@ -106,6 +112,7 @@ export type DeepReadonly<T> = T extends Builtin
 export const exhaustiveCheck = (param: never): never => {
   throw new Error(`Unreachable case: ${JSON.stringify(param)}`)
 }
+
 export type NonEmptyArray<T> = [T, ...T[]]
 export const isNonEmptyArray = <T>(arr: T[]): arr is NonEmptyArray<T> => {
   return arr.length > 0
diff --git a/packages/common/vitest.config.ts b/packages/common/vitest.config.ts
index ca880cb79..44d0c679d 100644
--- a/packages/common/vitest.config.ts
+++ b/packages/common/vitest.config.ts
@@ -10,10 +10,10 @@ export default defineConfig({
       thresholds: {
         // the endgame is to put thresholds at 100 and never touch it again :)
         autoUpdate: true,
-        branches: 91.85,
-        functions: 84.25,
-        lines: 96.87,
-        statements: 96.87,
+        branches: 92.21,
+        functions: 85.54,
+        lines: 97.29,
+        statements: 97.29,
         perFile: false,
       },
     },
-- 
GitLab