diff --git a/packages/api/src/api/rest/etapes.ts b/packages/api/src/api/rest/etapes.ts index 0a23459f7e90893e68886ea33417bff426f944d8..0c1c1d68e99dd688b7923bf2eca5a487067d1e6d 100644 --- a/packages/api/src/api/rest/etapes.ts +++ b/packages/api/src/api/rest/etapes.ts @@ -320,7 +320,7 @@ type PerimetreInfos = { surface: KM2 | null } & Pick<GraphqlEtape, 'geojson4326Forages' | 'geojsonOrigineForages'> & Pick<GetGeojsonInformation, 'communes' | 'forets' | 'departements'> -export const getPerimetreInfosInternal = ( +const getPerimetreInfosInternal = ( pool: Pool, geojson4326Perimetre: GraphqlEtape['geojson4326Perimetre'], geojsonOriginePerimetre: GraphqlEtape['geojsonOriginePerimetre'], diff --git a/packages/api/src/business/processes/titres-etapes-heritage-contenu-update.ts b/packages/api/src/business/processes/titres-etapes-heritage-contenu-update.ts index 24dca5acc8b8786e579710492916f7f9b65f66b7..71a1d203327c27d452c899216e444bfd04a7442a 100644 --- a/packages/api/src/business/processes/titres-etapes-heritage-contenu-update.ts +++ b/packages/api/src/business/processes/titres-etapes-heritage-contenu-update.ts @@ -84,9 +84,6 @@ export const titresEtapesHeritageContenuUpdate = async (pool: Pool, user: UserNo if (isNotNullNorUndefinedNorEmpty(titreEtapesPerdantLesSections)) { for (const etapePerdantLesSections of titreEtapesPerdantLesSections) { console.error(`l'étape https://camino.beta.gouv.fr/etapes/${etapePerdantLesSections.id} de type ${etapePerdantLesSections.typeId} possède un contenu alors qu'elle n'est pas censée en avoir`) - // TODO 2025-02-24 : à décommenter et à lancer en prod, puis à supprimer - // prettier-ignore - // await titreEtapeUpdate(etapePerdantLesSections.id, { contenu: null, heritageContenu: null, }, user, titreDemarche.titreId) titresEtapesIdsErrors.push(etapePerdantLesSections.id) } } diff --git a/packages/api/src/business/rules-demarches/procedure-specifique/procedure-specifique.machine.test.ts b/packages/api/src/business/rules-demarches/procedure-specifique/procedure-specifique.machine.test.ts index 531f0457f458ede4883ca0b0f54d8960356014d4..de264500817a1e590db8f739e06248494a922bff 100644 --- a/packages/api/src/business/rules-demarches/procedure-specifique/procedure-specifique.machine.test.ts +++ b/packages/api/src/business/rules-demarches/procedure-specifique/procedure-specifique.machine.test.ts @@ -288,6 +288,35 @@ describe('vérifie l’arbre des procédures spécifique', () => { ] `) }) + test("la démarche devient public dès la création d'une consultation du public à venir", () => { + const { tree } = setDateAndOrderAndInterpretMachine(psAxmProMachine, '1999-04-14', [ + { + ...ETES.demande.FAIT, + consentement: 'non-applicable', + concurrence: { amIFirst: true }, + paysId: 'FR', + surface: hectareToKm2(hectareValidator.parse(10)), + }, + ETES.enregistrementDeLaDemande.FAIT, + { ...ETES.recevabiliteDeLaDemande.FAVORABLE, hasTitreFrom: false }, + ETES.avisDesCollectivites.FAIT, + ETES.avisDesServicesEtCommissionsConsultatives.FAIT, + ETES.avisDuPrefet.FAVORABLE, + ETES.consultationDuPublic.PROGRAMME, + ]) + expect(tree).toMatchInlineSnapshot(` + [ + "RIEN (confidentielle, en construction ) -> [FAIRE_DEMANDE]", + "FAIRE_DEMANDE (confidentielle, en instruction ) -> [CLASSER_SANS_SUITE,DEMANDER_INFORMATION,DESISTER_PAR_LE_DEMANDEUR,ENREGISTRER_DEMANDE]", + "ENREGISTRER_DEMANDE (confidentielle, en instruction ) -> [CLASSER_SANS_SUITE,DEMANDER_INFORMATION,DESISTER_PAR_LE_DEMANDEUR,FAIRE_RECEVABILITE_DEFAVORABLE,FAIRE_RECEVABILITE_FAVORABLE]", + "FAIRE_RECEVABILITE_FAVORABLE (confidentielle, en instruction ) -> [CLASSER_SANS_SUITE,DEMANDER_INFORMATION,DESISTER_PAR_LE_DEMANDEUR,OUVRIR_CONSULTATION_DU_PUBLIC,OUVRIR_ENQUETE_PUBLIQUE,RENDRE_AVIS_COLLECTIVITES,RENDRE_AVIS_SERVICES_COMMISSIONS]", + "RENDRE_AVIS_COLLECTIVITES (confidentielle, en instruction ) -> [CLASSER_SANS_SUITE,DEMANDER_INFORMATION,DESISTER_PAR_LE_DEMANDEUR,OUVRIR_CONSULTATION_DU_PUBLIC,OUVRIR_ENQUETE_PUBLIQUE,RENDRE_AVIS_SERVICES_COMMISSIONS]", + "RENDRE_AVIS_SERVICES_COMMISSIONS (confidentielle, en instruction ) -> [CLASSER_SANS_SUITE,DEMANDER_INFORMATION,DESISTER_PAR_LE_DEMANDEUR,OUVRIR_CONSULTATION_DU_PUBLIC,OUVRIR_ENQUETE_PUBLIQUE,RENDRE_AVIS_PREFET]", + "RENDRE_AVIS_PREFET (confidentielle, en instruction ) -> [CLASSER_SANS_SUITE,DEMANDER_INFORMATION,DESISTER_PAR_LE_DEMANDEUR,OUVRIR_CONSULTATION_DU_PUBLIC,OUVRIR_ENQUETE_PUBLIQUE]", + "OUVRIR_CONSULTATION_DU_PUBLIC (publique , en instruction ) -> [CLASSER_SANS_SUITE,DEMANDER_INFORMATION,DESISTER_PAR_LE_DEMANDEUR]", + ] + `) + }) test("ne peut pas rendre la décision de l'administration si la consultation du public n'est pas terminée", () => { const { tree } = setDateAndOrderAndInterpretMachine(psAxmProMachine, '1999-04-14', [ { diff --git a/packages/api/src/business/rules-demarches/procedure-specifique/procedure-specifique.machine.ts b/packages/api/src/business/rules-demarches/procedure-specifique/procedure-specifique.machine.ts index 8d85d97ccc011abe93d1fa66a3600ea58dc6dab0..f3509b6bdbcd0265323f09c048a183bd848be70e 100644 --- a/packages/api/src/business/rules-demarches/procedure-specifique/procedure-specifique.machine.ts +++ b/packages/api/src/business/rules-demarches/procedure-specifique/procedure-specifique.machine.ts @@ -586,13 +586,9 @@ const procedureSpecifiqueMachine = (titreTypeId: TitreTypeId, demarcheTypeId: De }, { target: 'enAttente', - guard: and([or(['isEnquetePubliqueRequired', 'isEnquetePubliquePossible']), ({ event }) => event.status === ETAPES_STATUTS.EN_COURS]), + guard: and([or(['isEnquetePubliqueRequired', 'isEnquetePubliquePossible']), ({ event }) => [ETAPES_STATUTS.EN_COURS, ETAPES_STATUTS.PROGRAMME].includes(event.status)]), actions: assign({ visibilite: 'publique' }), }, - { - target: 'enAttente', - guard: and([or(['isEnquetePubliqueRequired', 'isEnquetePubliquePossible']), ({ event }) => event.status === ETAPES_STATUTS.PROGRAMME]), - }, ], OUVRIR_CONSULTATION_DU_PUBLIC: [ { @@ -602,14 +598,9 @@ const procedureSpecifiqueMachine = (titreTypeId: TitreTypeId, demarcheTypeId: De }, { target: 'enAttente', - guard: and([not('isEnquetePubliqueRequired'), ({ event }) => event.status === ETAPES_STATUTS.EN_COURS]), + guard: and([not('isEnquetePubliqueRequired'), ({ event }) => [ETAPES_STATUTS.PROGRAMME, ETAPES_STATUTS.EN_COURS].includes(event.status)]), actions: assign({ visibilite: 'publique', consultationDuPublicFaite: true }), }, - { - target: 'enAttente', - guard: and([not('isEnquetePubliqueRequired'), ({ event }) => event.status === ETAPES_STATUTS.PROGRAMME]), - actions: assign({ consultationDuPublicFaite: true }), - }, ], }, }, diff --git a/packages/api/src/business/rules-demarches/procedure-specifique/procedure-specifique.pdf b/packages/api/src/business/rules-demarches/procedure-specifique/procedure-specifique.pdf index c6f07dfb77cc8d9ae70badaecd0995170ab5980a..e2da5ed1b5a015c3338f14b3ce437d097704eae5 100644 Binary files a/packages/api/src/business/rules-demarches/procedure-specifique/procedure-specifique.pdf and b/packages/api/src/business/rules-demarches/procedure-specifique/procedure-specifique.pdf differ diff --git a/packages/api/src/scripts/check-perimetres.ts b/packages/api/src/scripts/check-perimetres.ts deleted file mode 100644 index 7e55e22b31962acdac5a9c8906380a4694b62546..0000000000000000000000000000000000000000 --- a/packages/api/src/scripts/check-perimetres.ts +++ /dev/null @@ -1,222 +0,0 @@ -import '../init' -import pg from 'pg' -import { config } from '../config/index' -import { Effect } from 'effect' -import { knex } from '../knex' -import { EtapeBrouillon, EtapeId } from 'camino-common/src/etape' -import { CaminoError } from 'camino-common/src/zod-tools' -import { getPerimetreInfosInternal } from '../api/rest/etapes' -import { TitreTypeId } from 'camino-common/src/static/titresTypes' -import { FeatureCollectionForages, FeatureCollectionPoints, FeatureMultiPolygon, MultiPolygon } from 'camino-common/src/perimetre' -import { GeoSystemeId } from 'camino-common/src/static/geoSystemes' -import { titreEtapeUpsert } from '../database/queries/titres-etapes' -import { userSuper } from '../database/user-super' -import { DemarcheId } from 'camino-common/src/demarche' -import { CaminoDate } from 'camino-common/src/date' -import { EtapeTypeId } from 'camino-common/src/static/etapesTypes' -import { EtapeStatutId } from 'camino-common/src/static/etapesStatuts' -import { TitreId } from 'camino-common/src/validators/titres' -import { callAndExit } from '../tools/fp-tools' - -// Le pool ne doit être qu'aux entrypoints : le daily, le monthly, et l'application. -const pool = new pg.Pool({ - host: config().PGHOST, - user: config().PGUSER, - password: config().PGPASSWORD, - database: config().PGDATABASE, -}) - -type QueryError = "Échec de récupération des ids d'étapes en BDD" -type EtapeError = 'Des étapes ont des périmètres invalides' -type PerimetreValidationError = "Échec de validation d'un périmètre" -type EtapeUploadError = "Échec du réupload d'un périmètre" -type PipelineError = CaminoError<QueryError | EtapeError | PerimetreValidationError | EtapeUploadError> - -type Perimetre = { - titreId: TitreId - titreTypeId: TitreTypeId - etapeId: EtapeId - demarcheId: DemarcheId - isBrouillon: EtapeBrouillon - date: CaminoDate - etapeTypeId: EtapeTypeId - etapeStatutId: EtapeStatutId - geojson4326Perimetre: MultiPolygon - geojsonOriginePerimetre: FeatureMultiPolygon | null - geojsonOriginePoints: FeatureCollectionPoints | null - geojsonOrigineGeoSystemeId: GeoSystemeId | null - geojsonOrigineForages: FeatureCollectionForages | null -} -function getAllPerimetres(): Effect.Effect<Perimetre[], CaminoError<QueryError>, never> { - return Effect.gen(function* () { - const { rows } = yield* Effect.tryPromise({ - try: () => - knex.raw<{ rows: Perimetre[] }>(` - SELECT - "titreId", - "titreTypeId", - "etapeId", - "demarcheId", - "isBrouillon", - "date", - "etapeTypeId", - "etapeStatutId", - ST_AsGeoJSON("geojson4326_perimetre", 40)::json AS "geojson4326Perimetre", - "geojsonOriginePerimetre", - "geojsonOriginePoints", - "geojsonOrigineGeoSystemeId", - "geojsonOrigineForages" - FROM ( - SELECT DISTINCT - t.id AS "titreId", - t.type_id AS "titreTypeId", - te.id AS "etapeId", - td.id AS "demarcheId", - te.is_brouillon AS "isBrouillon", - te.date, - te.type_id AS "etapeTypeId", - te.statut_id AS "etapeStatutId", - te.geojson4326_perimetre, - te.geojson_origine_perimetre AS "geojsonOriginePerimetre", - te.geojson_origine_points AS "geojsonOriginePoints", - te.geojson_origine_geo_systeme_id AS "geojsonOrigineGeoSystemeId", - te.geojson_origine_forages AS "geojsonOrigineForages" - FROM titres_etapes te - JOIN titres_demarches td ON td.id = te.titre_demarche_id - JOIN titres t ON t.id = td.titre_id - WHERE geojson4326_perimetre IS NOT NULL AND te.archive IS FALSE - ORDER BY te.date DESC - ) t - `), - catch: error => ({ - message: "Échec de récupération des ids d'étapes en BDD" as const, - extra: { error }, - }), - }) - - return rows - }) -} - -function updatePerimetre(perimetre: Perimetre): Effect.Effect<void, CaminoError<EtapeUploadError>, never> { - return Effect.tryPromise({ - try: async () => { - if (perimetre.geojsonOrigineGeoSystemeId !== '4326') { - throw new Error('Geosysteme invalide') - } - - await titreEtapeUpsert( - { - id: perimetre.etapeId, - typeId: perimetre.etapeTypeId, - statutId: perimetre.etapeStatutId, - date: perimetre.date, - isBrouillon: perimetre.isBrouillon, - titreDemarcheId: perimetre.demarcheId, - geojson4326Perimetre: { - type: 'Feature', - geometry: perimetre.geojson4326Perimetre, - properties: {}, - }, - geojsonOriginePerimetre: { - type: 'Feature', - geometry: perimetre.geojson4326Perimetre, - properties: {}, - }, - geojsonOriginePoints: perimetre.geojsonOriginePoints, - geojsonOrigineGeoSystemeId: perimetre.geojsonOrigineGeoSystemeId, - geojsonOrigineForages: perimetre.geojsonOrigineForages, - }, - userSuper, - perimetre.titreId - ) - }, - catch: error => ({ - message: "Échec du réupload d'un périmètre" as const, - extra: { - etapeId: perimetre.etapeId, - error, - }, - }), - }) -} - -type InvalidEtape = { id: EtapeId; errors: string[] } -function getInvalidEtapes(rows: Perimetre[]): Effect.Effect<InvalidEtape[], CaminoError<PerimetreValidationError | EtapeUploadError>, never> { - return Effect.gen(function* () { - const invalidEtapes: InvalidEtape[] = [] - - for (let i = 0; i < rows.length; i += 1) { - const errors = yield* getPerimetreErrors(rows[i]) - if (errors.length > 0) { - invalidEtapes.push({ id: rows[i].etapeId, errors }) - console.error(`(${Math.round((i / rows.length) * 10000) / 100}%) ${rows[i].etapeId} : ${errors.join(', ')}\n`) - - yield* updatePerimetre(rows[i]) - } - } - - return invalidEtapes - }) -} - -function getPerimetreErrors(perimetre: Perimetre): Effect.Effect<string[], CaminoError<PerimetreValidationError>, never> { - return Effect.tryPromise({ - try: async () => { - try { - await callAndExit( - getPerimetreInfosInternal( - pool, - { type: 'Feature', geometry: perimetre.geojson4326Perimetre, properties: {} }, - perimetre.geojsonOriginePerimetre, - perimetre.geojsonOriginePoints, - perimetre.titreTypeId, - perimetre.geojsonOrigineGeoSystemeId, - perimetre.geojsonOrigineForages - ) - ) - } catch (error) { - if (error instanceof Error) { - return [error.message] - } else if (typeof error === 'string') { - return [error] - } else { - return ['Périmètre invalide (raison inconnue)'] - } - } - - return [] - }, - catch: error => ({ - message: "Échec de validation d'un périmètre" as const, - extra: { - etapeId: perimetre.etapeId, - error: error instanceof Error ? error.message : error, - }, - }), - }) -} - -const pipeline: Effect.Effect<void, PipelineError, never> = Effect.gen(function* () { - console.time('PIPELINE') - const perimetres = yield* getAllPerimetres() - const invalidEtapes = yield* getInvalidEtapes(perimetres) - console.timeEnd('PIPELINE') - - if (invalidEtapes.length > 0) { - // eslint-disable-next-line no-console - console.log(`${invalidEtapes.length} ont des périmètres invalides : ${invalidEtapes.map(({ id }) => id).join(',')}`) - yield* Effect.fail({ - message: `Des étapes ont des périmètres invalides` as const, - extra: invalidEtapes.map(({ id, errors }) => `${id} : ${errors.join(', ')}`).join('\n'), - }) - } -}) - -try { - await Effect.runPromise(pipeline) - console.info('Script terminé : aucune erreur détectée') - process.exit() -} catch (error) { - process.exit(1) -}