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