From 075d9e8f95e672f1d60baa96a51e916585125ef7 Mon Sep 17 00:00:00 2001 From: Anis Safine Laget <anis.safine@beta.gouv.fr> Date: Thu, 13 Mar 2025 17:33:03 +0100 Subject: [PATCH 1/4] =?UTF-8?q?d=C3=A9but=20script=20de=20fix=20des=20prol?= =?UTF-8?q?ongations=20incompl=C3=A9tes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/api/package.json | 2 +- .../api/src/api/rest/demarches.queries.ts | 2 +- .../api/src/business/check-prolongations.ts | 284 ++++++++++++++++++ .../api/src/scripts/check-prolongations.ts | 14 + 4 files changed, 300 insertions(+), 2 deletions(-) create mode 100644 packages/api/src/business/check-prolongations.ts create mode 100644 packages/api/src/scripts/check-prolongations.ts diff --git a/packages/api/package.json b/packages/api/package.json index e38eef6ed..75cb20513 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -7,7 +7,7 @@ "type": "module", "scripts": { "build": "tsc --incremental", - "check-perimetres": "node --enable-source-maps --loader ts-node/esm/transpile-only ./src/scripts/check-perimetres.ts", + "check-prolongations": "node --enable-source-maps --loader ts-node/esm/transpile-only ./src/scripts/check-prolongations.ts", "daily": "node --enable-source-maps --loader ts-node/esm/transpile-only ./src/scripts/daily.ts", "monthly": "node --enable-source-maps --loader ts-node/esm/transpile-only ./src/scripts/monthly.ts", "db:dump": "rm -rf ./backups/* && pg_dump --host=localhost --username=postgres --clean --if-exists --format=d --no-owner --no-privileges --dbname=camino --file=./backups/", diff --git a/packages/api/src/api/rest/demarches.queries.ts b/packages/api/src/api/rest/demarches.queries.ts index 9623f992d..18f583930 100644 --- a/packages/api/src/api/rest/demarches.queries.ts +++ b/packages/api/src/api/rest/demarches.queries.ts @@ -435,7 +435,7 @@ FROM ( ) tmp ` const getDemarchesValidator = z.object({ id: demarcheIdValidator }) -type GetDemarche = z.infer<typeof getDemarchesValidator> +export type GetDemarche = z.infer<typeof getDemarchesValidator> export const getDemarches = (pool: Pool): Effect.Effect<GetDemarche[], CaminoError<EffectDbQueryAndValidateErrors>> => effectDbQueryAndValidate(getDemarchesDb, {}, pool, getDemarchesValidator) const getDemarchesDb = sql<Redefine<IGetDemarchesDbQuery, {}, GetDemarche>>` diff --git a/packages/api/src/business/check-prolongations.ts b/packages/api/src/business/check-prolongations.ts new file mode 100644 index 000000000..9e80e263a --- /dev/null +++ b/packages/api/src/business/check-prolongations.ts @@ -0,0 +1,284 @@ +import { Pool } from 'pg' +import { getDemarcheByIdOrSlug, GetDemarcheByIdOrSlugErrors, getDemarches, getEtapesByDemarcheId } from '../api/rest/demarches.queries' +import { Effect, pipe } from 'effect' +import { CaminoError } from 'camino-common/src/zod-tools' +import { DemarcheId } from 'camino-common/src/demarche' +import { isEtapeComplete } from 'camino-common/src/permissions/titres-etapes' +import { userSuper } from '../database/user-super' +import { CaminoDate, firstEtapeDateValidator } from 'camino-common/src/date' +import { titreEtapeGet, titreEtapeUpsert } from '../database/queries/titres-etapes' +import { isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty, isNullOrUndefined, isNullOrUndefinedOrEmpty, memoize, toSorted } from 'camino-common/src/typescript-tools' +import { iTitreEtapeToFlattenEtape, TitreEtapeToFlattenEtapeErrors } from '../api/_format/titres-etapes' +import { ITitreEtape } from '../types' +import { + getDocumentsByEtapeId, + getEntrepriseDocumentIdsByEtapeId, + getEtapeAvisLargeObjectIdsByEtapeId, + GetEtapeAvisLargeObjectIdsByEtapeIdErrors, + GetEtapeDocumentLargeObjectIdsByEtapeIdErrors, +} from '../database/queries/titres-etapes.queries' +import { getAdministrationsLocales } from 'camino-common/src/administrations' +import { ZodUnparseable } from '../tools/fp-tools' +import { TitreId, TitreSlug } from 'camino-common/src/validators/titres' +import { EtapeId } from 'camino-common/src/etape' +import { isDemarcheTypeProlongations } from 'camino-common/src/static/demarchesTypes' +import { SUBSTANCES_FISCALES_IDS } from 'camino-common/src/static/substancesFiscales' +import { entrepriseIdValidator } from 'camino-common/src/entreprise' +import { km2Validator } from 'camino-common/src/number' +import { FeatureMultiPolygon } from 'camino-common/src/perimetre' + +type CheckedProlongation = { + titre_slug: TitreSlug | null + demarche_id: DemarcheId + etape_id: EtapeId | null + etape_date: CaminoDate | null + titre_id: TitreId | null + errors: string[] +} + +const demarchePasProlongationError = 'pas une prolongation' as const +const demarcheSansEtapesError = "La démarche ne contient pas d'étapes" as const +const titreEtapeGetError = 'Le fetch de la firstEtape a échoué' as const +type CheckOnePrologationErrors = + | GetDemarcheByIdOrSlugErrors + | TitreEtapeToFlattenEtapeErrors + | GetEtapeDocumentLargeObjectIdsByEtapeIdErrors + | ZodUnparseable + | GetEtapeAvisLargeObjectIdsByEtapeIdErrors + | typeof titreEtapeGetError + | typeof demarcheSansEtapesError + | typeof demarchePasProlongationError +const checkOneProlongation = (pool: Pool, demarcheId: DemarcheId): Effect.Effect<CheckedProlongation, CaminoError<CheckOnePrologationErrors>> => { + return Effect.Do.pipe( + Effect.bind('demarche', () => getDemarcheByIdOrSlug(pool, demarcheId)), + Effect.filterOrFail( + ({ demarche }) => isDemarcheTypeProlongations(demarche.demarche_type_id), + () => ({ message: demarchePasProlongationError }) + ), + Effect.bind('etapes', () => getEtapesByDemarcheId(pool, demarcheId)), + Effect.filterOrFail( + ({ etapes }) => isNotNullNorUndefinedNorEmpty(etapes), + () => ({ message: demarcheSansEtapesError, extra: {} }) + ), + Effect.bind('firstEtape', ({ etapes }) => + pipe( + Effect.tryPromise({ + try: () => titreEtapeGet(toSorted(etapes, (a, b) => a.ordre - b.ordre)[0].id, { fields: { id: {} }, fetchHeritage: true }, userSuper), + catch: error => ({ message: titreEtapeGetError, extra: error }), + }), + Effect.filterOrFail( + (firstEtape): firstEtape is ITitreEtape => isNotNullNorUndefined(firstEtape), + () => ({ message: titreEtapeGetError, extra: 'La first étape est null ou undefined' }) + ) + ) + ), + Effect.let('titreProps', ({ demarche, firstEtape }) => ({ + titreTypeId: memoize(() => Promise.resolve(demarche.titre_type_id)), + administrationsLocales: memoize(() => + Promise.resolve(getAdministrationsLocales(isNotNullNorUndefined(firstEtape.communes) ? firstEtape.communes.map(({ id }) => id) : [], firstEtape.secteursMaritime)) + ), + entreprisesTitulairesOuAmodiataires: memoize(() => Promise.resolve([...(firstEtape.titulaireIds ?? []), ...(firstEtape.amodiataireIds ?? [])])), + })), + Effect.bind('etapeDocuments', ({ firstEtape, demarche, titreProps }) => { + return getDocumentsByEtapeId(firstEtape.id, pool, userSuper, titreProps.titreTypeId, titreProps.administrationsLocales, titreProps.entreprisesTitulairesOuAmodiataires, firstEtape.typeId, { + demarche_type_id: demarche.demarche_type_id, + entreprises_lecture: demarche.entreprises_lecture, + public_lecture: demarche.public_lecture, + titre_public_lecture: false, + }) + }), + Effect.bind('entrepriseDocuments', ({ firstEtape }) => getEntrepriseDocumentIdsByEtapeId({ titre_etape_id: firstEtape.id }, pool, userSuper)), + Effect.bind('etapeAvis', ({ firstEtape, demarche, titreProps }) => + getEtapeAvisLargeObjectIdsByEtapeId( + firstEtape.id, + pool, + userSuper, + titreProps.titreTypeId, + titreProps.administrationsLocales, + titreProps.entreprisesTitulairesOuAmodiataires, + firstEtape.typeId, + { + demarche_type_id: demarche.demarche_type_id, + entreprises_lecture: demarche.entreprises_lecture, + public_lecture: demarche.public_lecture, + titre_public_lecture: false, + } + ) + ), + Effect.bind('flattenFirstEtape', ({ firstEtape }) => iTitreEtapeToFlattenEtape(firstEtape)), + Effect.map(({ demarche, firstEtape, flattenFirstEtape, etapeDocuments, entrepriseDocuments, etapeAvis }) => { + const isFirstEtapeComplete = isEtapeComplete( + flattenFirstEtape, + demarche.titre_type_id, + demarcheId, + demarche.demarche_type_id, + etapeDocuments, + entrepriseDocuments, + firstEtape.sdomZones, + isNotNullNorUndefined(firstEtape.communes) ? firstEtape.communes.map(({ id }) => id) : [], + etapeAvis, + userSuper, + firstEtapeDateValidator.parse(flattenFirstEtape.date) + ) + + return { + titre_slug: demarche.titre_slug, + demarche_id: demarche.demarche_id, + etape_id: firstEtape.id, + etape_date: firstEtape.date, + titre_id: demarche.titre_id, + errors: isFirstEtapeComplete.valid ? [] : isFirstEtapeComplete.errors, + } + }), + Effect.catchAll(caminoError => { + return Effect.succeed({ + titre_slug: null, + demarche_id: demarcheId, + etape_id: null, + etape_date: null, + titre_id: null, + errors: [caminoError.message], + }) + }) + ) +} + +const multiPolygonWith4Points: FeatureMultiPolygon = { + type: 'Feature', + properties: {}, + geometry: { + type: 'MultiPolygon', + coordinates: [ + [ + [ + [-53.16822754488772, 5.02935254143807], + [-53.15913163720232, 5.029382753429523], + [-53.15910186841349, 5.020342601941031], + [-53.168197650929095, 5.02031244452273], + [-53.16822754488772, 5.02935254143807], + ], + ], + ], + }, +} + +const etapeIdVideError = "Pas d'id d'étape" as const +const titreIdVideError = "Pas d'id de titre" as const +const upsertFailError = "Échec de mise à jour de l'étape" as const +const dateVideError = "Pas de date d'étape" as const +type FixProlongationErrors = typeof etapeIdVideError | typeof titreIdVideError | typeof upsertFailError | typeof dateVideError +const fixProlongation = (checkedProlongation: CheckedProlongation): Effect.Effect<null, CaminoError<FixProlongationErrors>> => { + return Effect.Do.pipe( + Effect.bind('etapeId', () => + pipe( + Effect.succeed(checkedProlongation.etape_id), + Effect.filterOrFail( + (etapeId): etapeId is EtapeId => isNotNullNorUndefined(etapeId), + () => ({ message: etapeIdVideError }) + ) + ) + ), + Effect.bind('titreId', () => + pipe( + Effect.succeed(checkedProlongation.titre_id), + Effect.filterOrFail( + (titreId): titreId is TitreId => isNotNullNorUndefined(titreId), + () => ({ message: titreIdVideError }) + ) + ) + ), + Effect.bind('etapeDate', () => + pipe( + Effect.succeed(checkedProlongation.etape_date), + Effect.filterOrFail( + (date): date is CaminoDate => isNotNullNorUndefined(date), + () => ({ message: dateVideError }) + ) + ) + ), + Effect.flatMap(({ etapeId, titreId, etapeDate }) => + Effect.tryPromise({ + try: () => + titreEtapeUpsert( + { + id: etapeId, + titreDemarcheId: checkedProlongation.demarche_id, + date: etapeDate, + substances: [SUBSTANCES_FISCALES_IDS.or], + titulaireIds: [entrepriseIdValidator.parse('fr-325313195')], + surface: km2Validator.parse(42), + geojson4326Perimetre: multiPolygonWith4Points, + geojsonOriginePerimetre: multiPolygonWith4Points, + geojsonOrigineGeoSystemeId: '4326', + }, + userSuper, + titreId + ), + catch: error => ({ message: upsertFailError, extra: error }), + }) + ), + Effect.map(() => null), + Effect.catchAll((error) => { + console.log(error) + return Effect.succeed(null) + }) + ) +} + +const toPercentage = (percentage: number): string => { + return `${Math.round(percentage * 10000) / 100} %` +} + +export const checkProlongations = (pool: Pool): Effect.Effect<CheckedProlongation[], CaminoError<CheckOnePrologationErrors | FixProlongationErrors>> => { + return pipe( + getDemarches(pool), + Effect.flatMap(demarches => { + const initialAcc: CheckedProlongation[] = [] + return Effect.reduce( + demarches.map(({ id }) => id), + initialAcc, + (acc, demarcheId) => { + return pipe( + checkOneProlongation(pool, demarcheId), + Effect.tap(checkedProlongation => { + if (isNullOrUndefinedOrEmpty(checkedProlongation.errors) || checkedProlongation.errors.includes(demarchePasProlongationError)) { + return Effect.succeed(checkedProlongation) + } + + return fixProlongation(checkedProlongation) + }), + Effect.map(checkedProlongation => { + if (isNotNullNorUndefinedNorEmpty(checkedProlongation.errors) && !checkedProlongation.errors.includes(demarchePasProlongationError)) { + const link = isNotNullNorUndefined(checkedProlongation.titre_slug) + ? `http://localhost:4180/titres/${checkedProlongation.titre_slug}` + : `http://localhost:4180/demarches/${checkedProlongation.demarche_id}` + console.log(`${link} : ${checkedProlongation.errors.join(' | ')}`) + } + + return [...acc, checkedProlongation] + }) + ) + } + ) + }), + Effect.tap(checkedProlongation => { + const prolongations = checkedProlongation.filter(({ errors }) => !errors.includes(demarchePasProlongationError)) + const prolongationsEnVrac = prolongations.filter(({ errors }) => errors.length > 0) + const prolongationsAvecDureeManquante = prolongations.filter(({ errors }) => errors.some(error => error.includes('la durée est obligatoire'))) + + const prolongationsParType = prolongationsEnVrac.reduce<{ [key: string]: number }>((acc, prolongation) => { + const domaine = isNotNullNorUndefined(prolongation.titre_slug) ? `${prolongation.titre_slug.slice(2, 4)}${ prolongation.titre_slug.slice(0, 1)}` : 'inconnu' + if (isNullOrUndefined(acc[domaine])) { + acc[domaine] = 1 + } else { + acc[domaine] += 1 + } + + return acc + }, {}) + console.log(`Prolongations en vrac : ${prolongationsEnVrac.length} (${toPercentage(prolongationsEnVrac.length / prolongations.length)})`) + console.log(JSON.stringify(prolongationsParType, null, 2)) + console.log(`Prolongations sans durée : ${prolongationsAvecDureeManquante.length} (${toPercentage(prolongationsAvecDureeManquante.length / prolongationsEnVrac.length)})`) + }) + ) +} diff --git a/packages/api/src/scripts/check-prolongations.ts b/packages/api/src/scripts/check-prolongations.ts new file mode 100644 index 000000000..fa142e25b --- /dev/null +++ b/packages/api/src/scripts/check-prolongations.ts @@ -0,0 +1,14 @@ +import '../init' +import pg from 'pg' +import { config } from '../config' +import { checkProlongations } from '../business/check-prolongations' +import { Effect } from 'effect' + +const pool = new pg.Pool({ + host: config().PGHOST, + user: config().PGUSER, + password: config().PGPASSWORD, + database: config().PGDATABASE, +}) + +Effect.runPromiseExit(checkProlongations(pool)).catch(console.error) -- GitLab From 7259a28080c46b62eebf3fdda51df3030b9bf9fe Mon Sep 17 00:00:00 2001 From: Anis Safine Laget <anis.safine@beta.gouv.fr> Date: Mon, 17 Mar 2025 11:53:36 +0100 Subject: [PATCH 2/4] =?UTF-8?q?met=20=C3=A0=20jour=20avec=20les=20bonnes?= =?UTF-8?q?=20donn=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/src/business/check-prolongations.ts | 152 +++++++++--------- 1 file changed, 78 insertions(+), 74 deletions(-) diff --git a/packages/api/src/business/check-prolongations.ts b/packages/api/src/business/check-prolongations.ts index 9e80e263a..f64959723 100644 --- a/packages/api/src/business/check-prolongations.ts +++ b/packages/api/src/business/check-prolongations.ts @@ -5,7 +5,7 @@ import { CaminoError } from 'camino-common/src/zod-tools' import { DemarcheId } from 'camino-common/src/demarche' import { isEtapeComplete } from 'camino-common/src/permissions/titres-etapes' import { userSuper } from '../database/user-super' -import { CaminoDate, firstEtapeDateValidator } from 'camino-common/src/date' +import { firstEtapeDateValidator } from 'camino-common/src/date' import { titreEtapeGet, titreEtapeUpsert } from '../database/queries/titres-etapes' import { isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty, isNullOrUndefined, isNullOrUndefinedOrEmpty, memoize, toSorted } from 'camino-common/src/typescript-tools' import { iTitreEtapeToFlattenEtape, TitreEtapeToFlattenEtapeErrors } from '../api/_format/titres-etapes' @@ -22,16 +22,14 @@ import { ZodUnparseable } from '../tools/fp-tools' import { TitreId, TitreSlug } from 'camino-common/src/validators/titres' import { EtapeId } from 'camino-common/src/etape' import { isDemarcheTypeProlongations } from 'camino-common/src/static/demarchesTypes' -import { SUBSTANCES_FISCALES_IDS } from 'camino-common/src/static/substancesFiscales' -import { entrepriseIdValidator } from 'camino-common/src/entreprise' -import { km2Validator } from 'camino-common/src/number' -import { FeatureMultiPolygon } from 'camino-common/src/perimetre' +import { getMostRecentValuePropFromEtapeFondamentaleValide, TitreGetDemarche } from 'camino-common/src/titres' +import { isDemarcheStatutNonStatue, isDemarcheStatutNonValide } from 'camino-common/src/static/demarchesStatuts' +import { getTitre } from '../api/rest/titres.queries' type CheckedProlongation = { titre_slug: TitreSlug | null demarche_id: DemarcheId etape_id: EtapeId | null - etape_date: CaminoDate | null titre_id: TitreId | null errors: string[] } @@ -125,7 +123,6 @@ const checkOneProlongation = (pool: Pool, demarcheId: DemarcheId): Effect.Effect titre_slug: demarche.titre_slug, demarche_id: demarche.demarche_id, etape_id: firstEtape.id, - etape_date: firstEtape.date, titre_id: demarche.titre_id, errors: isFirstEtapeComplete.valid ? [] : isFirstEtapeComplete.errors, } @@ -135,7 +132,6 @@ const checkOneProlongation = (pool: Pool, demarcheId: DemarcheId): Effect.Effect titre_slug: null, demarche_id: demarcheId, etape_id: null, - etape_date: null, titre_id: null, errors: [caminoError.message], }) @@ -143,77 +139,85 @@ const checkOneProlongation = (pool: Pool, demarcheId: DemarcheId): Effect.Effect ) } -const multiPolygonWith4Points: FeatureMultiPolygon = { - type: 'Feature', - properties: {}, - geometry: { - type: 'MultiPolygon', - coordinates: [ - [ - [ - [-53.16822754488772, 5.02935254143807], - [-53.15913163720232, 5.029382753429523], - [-53.15910186841349, 5.020342601941031], - [-53.168197650929095, 5.02031244452273], - [-53.16822754488772, 5.02935254143807], - ], - ], - ], - }, -} - -const etapeIdVideError = "Pas d'id d'étape" as const const titreIdVideError = "Pas d'id de titre" as const const upsertFailError = "Échec de mise à jour de l'étape" as const -const dateVideError = "Pas de date d'étape" as const -type FixProlongationErrors = typeof etapeIdVideError | typeof titreIdVideError | typeof upsertFailError | typeof dateVideError -const fixProlongation = (checkedProlongation: CheckedProlongation): Effect.Effect<null, CaminoError<FixProlongationErrors>> => { +const titreGetError = "Impossible de fetch le titre" as const +const titreGetIsNullError = "Titre introuvable" as const +const etapeIntrouvableError = "Étape introuvable dans le titre" as const +const demarcheListIsEmptyError = "Liste des démarches héritables vide" as const +type FixProlongationErrors = typeof titreIdVideError | typeof upsertFailError | typeof titreGetError | typeof titreGetIsNullError | typeof demarcheListIsEmptyError | typeof etapeIntrouvableError +const fixProlongation = (pool: Pool, checkedProlongation: CheckedProlongation): Effect.Effect<null, CaminoError<FixProlongationErrors>> => { return Effect.Do.pipe( - Effect.bind('etapeId', () => - pipe( - Effect.succeed(checkedProlongation.etape_id), - Effect.filterOrFail( - (etapeId): etapeId is EtapeId => isNotNullNorUndefined(etapeId), - () => ({ message: etapeIdVideError }) - ) + Effect.bind('titre', () => pipe( + Effect.succeed(checkedProlongation.titre_id), + Effect.filterOrFail( + (titreId) => isNotNullNorUndefined(titreId), + () => ({ message: titreIdVideError }) + ), + Effect.flatMap((titreId) => Effect.tryPromise({ + try: () => getTitre(pool, userSuper, titreId), + catch: (error) => ({ message: titreGetError, extra: error }) + })), + Effect.filterOrFail( + (titre): titre is NonNullable<typeof titre> => isNotNullNorUndefined(titre), + (error) => ({ message: titreGetIsNullError, extra: error }) ) - ), - Effect.bind('titreId', () => - pipe( - Effect.succeed(checkedProlongation.titre_id), - Effect.filterOrFail( - (titreId): titreId is TitreId => isNotNullNorUndefined(titreId), - () => ({ message: titreIdVideError }) - ) + )), + Effect.bind('etape', ({ titre }) => pipe( + Effect.succeed(titre.demarches.find(({ id }) => id === checkedProlongation.demarche_id)), + Effect.filterOrFail( + (demarche): demarche is NonNullable<typeof demarche> => isNotNullNorUndefined(checkedProlongation.demarche_id) && isNotNullNorUndefined(demarche), + () => ({ message: etapeIntrouvableError, extra: 'Démarche introuvable' }) + ), + Effect.map((demarche) => demarche.etapes.find(({ id }) => id === checkedProlongation.etape_id)), + Effect.filterOrFail( + (etape): etape is NonNullable<typeof etape> => isNotNullNorUndefined(checkedProlongation.etape_id) && isNotNullNorUndefined(etape), + () => ({ message: etapeIntrouvableError, extra: 'Étape introuvable' }) ) - ), - Effect.bind('etapeDate', () => - pipe( - Effect.succeed(checkedProlongation.etape_date), - Effect.filterOrFail( - (date): date is CaminoDate => isNotNullNorUndefined(date), - () => ({ message: dateVideError }) - ) + )), + Effect.bind('propsHeritees', ({ titre }) => + Effect.Do.pipe( + Effect.bind('demarches', () => { + const demarches: TitreGetDemarche[] = [] + for (const demarche of toSorted([...titre.demarches], (a, b) => a.ordre - b.ordre)) { + if (demarche.id === checkedProlongation.demarche_id) { + return Effect.succeed([...demarches, demarche]) + } else if (!isDemarcheStatutNonStatue(demarche.demarche_statut_id) && !isDemarcheStatutNonValide(demarche.demarche_statut_id)) { + demarches.push(demarche) + } + } + + return Effect.fail({ message: demarcheListIsEmptyError }) + }), + Effect.map(({ demarches }) => { + return { + substances: getMostRecentValuePropFromEtapeFondamentaleValide('substances', demarches), + titulaireIds: getMostRecentValuePropFromEtapeFondamentaleValide('titulaireIds', demarches), + perimetre: getMostRecentValuePropFromEtapeFondamentaleValide('perimetre', demarches), + } + }) ) ), - Effect.flatMap(({ etapeId, titreId, etapeDate }) => + Effect.flatMap(({ titre, etape, propsHeritees }) => Effect.tryPromise({ - try: () => - titreEtapeUpsert( + try: () => { + console.log(`Trying to fix http://localhost:4180/etapes/${etape.id}`) + return titreEtapeUpsert( { - id: etapeId, + id: etape.id, titreDemarcheId: checkedProlongation.demarche_id, - date: etapeDate, - substances: [SUBSTANCES_FISCALES_IDS.or], - titulaireIds: [entrepriseIdValidator.parse('fr-325313195')], - surface: km2Validator.parse(42), - geojson4326Perimetre: multiPolygonWith4Points, - geojsonOriginePerimetre: multiPolygonWith4Points, - geojsonOrigineGeoSystemeId: '4326', + date: etape.date, + substances: 'fondamentale' in etape && isNotNullNorUndefinedNorEmpty(etape.fondamentale.substances) ? etape.fondamentale.substances : propsHeritees.substances, + titulaireIds: 'fondamentale' in etape && isNotNullNorUndefinedNorEmpty(etape.fondamentale.titulaireIds) ? etape.fondamentale.titulaireIds : propsHeritees.titulaireIds, + surface: 'fondamentale' in etape && isNotNullNorUndefined(etape.fondamentale.perimetre?.surface) ? etape.fondamentale.perimetre.surface : propsHeritees.perimetre?.surface, + geojson4326Perimetre: 'fondamentale' in etape && isNotNullNorUndefined(etape.fondamentale.perimetre?.geojson4326_perimetre) ? etape.fondamentale.perimetre.geojson4326_perimetre : propsHeritees.perimetre?.geojson4326_perimetre, + geojsonOriginePerimetre: 'fondamentale' in etape && isNotNullNorUndefined(etape.fondamentale.perimetre?.geojson_origine_perimetre) ? etape.fondamentale.perimetre.geojson_origine_perimetre : propsHeritees.perimetre?.geojson_origine_perimetre, + geojsonOrigineGeoSystemeId: 'fondamentale' in etape && isNotNullNorUndefined(etape.fondamentale.perimetre?.geojson_origine_geo_systeme_id) ? etape.fondamentale.perimetre.geojson_origine_geo_systeme_id : propsHeritees.perimetre?.geojson_origine_geo_systeme_id, }, userSuper, - titreId - ), + titre.id + ) + }, catch: error => ({ message: upsertFailError, extra: error }), }) ), @@ -245,15 +249,15 @@ export const checkProlongations = (pool: Pool): Effect.Effect<CheckedProlongatio return Effect.succeed(checkedProlongation) } - return fixProlongation(checkedProlongation) + return fixProlongation(pool, checkedProlongation) }), Effect.map(checkedProlongation => { - if (isNotNullNorUndefinedNorEmpty(checkedProlongation.errors) && !checkedProlongation.errors.includes(demarchePasProlongationError)) { - const link = isNotNullNorUndefined(checkedProlongation.titre_slug) - ? `http://localhost:4180/titres/${checkedProlongation.titre_slug}` - : `http://localhost:4180/demarches/${checkedProlongation.demarche_id}` - console.log(`${link} : ${checkedProlongation.errors.join(' | ')}`) - } + // if (isNotNullNorUndefinedNorEmpty(checkedProlongation.errors) && !checkedProlongation.errors.includes(demarchePasProlongationError)) { + // const link = isNotNullNorUndefined(checkedProlongation.titre_slug) + // ? `http://localhost:4180/titres/${checkedProlongation.titre_slug}` + // : `http://localhost:4180/demarches/${checkedProlongation.demarche_id}` + // console.log(`${link} : ${checkedProlongation.errors.join(' | ')}`) + // } return [...acc, checkedProlongation] }) -- GitLab From 82e28cba0fe01b7b1e3ac56f4a0a1f3e449b6b20 Mon Sep 17 00:00:00 2001 From: Anis Safine Laget <anis.safine@beta.gouv.fr> Date: Mon, 17 Mar 2025 16:21:59 +0100 Subject: [PATCH 3/4] fix ci --- packages/api/src/api/rest/demarches.queries.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/src/api/rest/demarches.queries.ts b/packages/api/src/api/rest/demarches.queries.ts index 18f583930..9623f992d 100644 --- a/packages/api/src/api/rest/demarches.queries.ts +++ b/packages/api/src/api/rest/demarches.queries.ts @@ -435,7 +435,7 @@ FROM ( ) tmp ` const getDemarchesValidator = z.object({ id: demarcheIdValidator }) -export type GetDemarche = z.infer<typeof getDemarchesValidator> +type GetDemarche = z.infer<typeof getDemarchesValidator> export const getDemarches = (pool: Pool): Effect.Effect<GetDemarche[], CaminoError<EffectDbQueryAndValidateErrors>> => effectDbQueryAndValidate(getDemarchesDb, {}, pool, getDemarchesValidator) const getDemarchesDb = sql<Redefine<IGetDemarchesDbQuery, {}, GetDemarche>>` -- GitLab From f69fc900d2ad51df4d8abc42746d6aead9403b3a Mon Sep 17 00:00:00 2001 From: Anis Safine Laget <anis.safine@beta.gouv.fr> Date: Mon, 17 Mar 2025 16:23:47 +0100 Subject: [PATCH 4/4] fix ci --- .../api/src/business/check-prolongations.ts | 95 +++++++++++-------- 1 file changed, 55 insertions(+), 40 deletions(-) diff --git a/packages/api/src/business/check-prolongations.ts b/packages/api/src/business/check-prolongations.ts index f64959723..8f378675d 100644 --- a/packages/api/src/business/check-prolongations.ts +++ b/packages/api/src/business/check-prolongations.ts @@ -141,40 +141,46 @@ const checkOneProlongation = (pool: Pool, demarcheId: DemarcheId): Effect.Effect const titreIdVideError = "Pas d'id de titre" as const const upsertFailError = "Échec de mise à jour de l'étape" as const -const titreGetError = "Impossible de fetch le titre" as const -const titreGetIsNullError = "Titre introuvable" as const -const etapeIntrouvableError = "Étape introuvable dans le titre" as const -const demarcheListIsEmptyError = "Liste des démarches héritables vide" as const +const titreGetError = 'Impossible de fetch le titre' as const +const titreGetIsNullError = 'Titre introuvable' as const +const etapeIntrouvableError = 'Étape introuvable dans le titre' as const +const demarcheListIsEmptyError = 'Liste des démarches héritables vide' as const type FixProlongationErrors = typeof titreIdVideError | typeof upsertFailError | typeof titreGetError | typeof titreGetIsNullError | typeof demarcheListIsEmptyError | typeof etapeIntrouvableError const fixProlongation = (pool: Pool, checkedProlongation: CheckedProlongation): Effect.Effect<null, CaminoError<FixProlongationErrors>> => { return Effect.Do.pipe( - Effect.bind('titre', () => pipe( - Effect.succeed(checkedProlongation.titre_id), - Effect.filterOrFail( - (titreId) => isNotNullNorUndefined(titreId), - () => ({ message: titreIdVideError }) - ), - Effect.flatMap((titreId) => Effect.tryPromise({ - try: () => getTitre(pool, userSuper, titreId), - catch: (error) => ({ message: titreGetError, extra: error }) - })), - Effect.filterOrFail( - (titre): titre is NonNullable<typeof titre> => isNotNullNorUndefined(titre), - (error) => ({ message: titreGetIsNullError, extra: error }) + Effect.bind('titre', () => + pipe( + Effect.succeed(checkedProlongation.titre_id), + Effect.filterOrFail( + titreId => isNotNullNorUndefined(titreId), + () => ({ message: titreIdVideError }) + ), + Effect.flatMap(titreId => + Effect.tryPromise({ + try: () => getTitre(pool, userSuper, titreId), + catch: error => ({ message: titreGetError, extra: error }), + }) + ), + Effect.filterOrFail( + (titre): titre is NonNullable<typeof titre> => isNotNullNorUndefined(titre), + error => ({ message: titreGetIsNullError, extra: error }) + ) ) - )), - Effect.bind('etape', ({ titre }) => pipe( - Effect.succeed(titre.demarches.find(({ id }) => id === checkedProlongation.demarche_id)), - Effect.filterOrFail( - (demarche): demarche is NonNullable<typeof demarche> => isNotNullNorUndefined(checkedProlongation.demarche_id) && isNotNullNorUndefined(demarche), - () => ({ message: etapeIntrouvableError, extra: 'Démarche introuvable' }) - ), - Effect.map((demarche) => demarche.etapes.find(({ id }) => id === checkedProlongation.etape_id)), - Effect.filterOrFail( - (etape): etape is NonNullable<typeof etape> => isNotNullNorUndefined(checkedProlongation.etape_id) && isNotNullNorUndefined(etape), - () => ({ message: etapeIntrouvableError, extra: 'Étape introuvable' }) + ), + Effect.bind('etape', ({ titre }) => + pipe( + Effect.succeed(titre.demarches.find(({ id }) => id === checkedProlongation.demarche_id)), + Effect.filterOrFail( + (demarche): demarche is NonNullable<typeof demarche> => isNotNullNorUndefined(checkedProlongation.demarche_id) && isNotNullNorUndefined(demarche), + () => ({ message: etapeIntrouvableError, extra: 'Démarche introuvable' }) + ), + Effect.map(demarche => demarche.etapes.find(({ id }) => id === checkedProlongation.etape_id)), + Effect.filterOrFail( + (etape): etape is NonNullable<typeof etape> => isNotNullNorUndefined(checkedProlongation.etape_id) && isNotNullNorUndefined(etape), + () => ({ message: etapeIntrouvableError, extra: 'Étape introuvable' }) + ) ) - )), + ), Effect.bind('propsHeritees', ({ titre }) => Effect.Do.pipe( Effect.bind('demarches', () => { @@ -201,7 +207,7 @@ const fixProlongation = (pool: Pool, checkedProlongation: CheckedProlongation): Effect.flatMap(({ titre, etape, propsHeritees }) => Effect.tryPromise({ try: () => { - console.log(`Trying to fix http://localhost:4180/etapes/${etape.id}`) + console.info(`Trying to fix http://localhost:4180/etapes/${etape.id}`) return titreEtapeUpsert( { id: etape.id, @@ -210,9 +216,18 @@ const fixProlongation = (pool: Pool, checkedProlongation: CheckedProlongation): substances: 'fondamentale' in etape && isNotNullNorUndefinedNorEmpty(etape.fondamentale.substances) ? etape.fondamentale.substances : propsHeritees.substances, titulaireIds: 'fondamentale' in etape && isNotNullNorUndefinedNorEmpty(etape.fondamentale.titulaireIds) ? etape.fondamentale.titulaireIds : propsHeritees.titulaireIds, surface: 'fondamentale' in etape && isNotNullNorUndefined(etape.fondamentale.perimetre?.surface) ? etape.fondamentale.perimetre.surface : propsHeritees.perimetre?.surface, - geojson4326Perimetre: 'fondamentale' in etape && isNotNullNorUndefined(etape.fondamentale.perimetre?.geojson4326_perimetre) ? etape.fondamentale.perimetre.geojson4326_perimetre : propsHeritees.perimetre?.geojson4326_perimetre, - geojsonOriginePerimetre: 'fondamentale' in etape && isNotNullNorUndefined(etape.fondamentale.perimetre?.geojson_origine_perimetre) ? etape.fondamentale.perimetre.geojson_origine_perimetre : propsHeritees.perimetre?.geojson_origine_perimetre, - geojsonOrigineGeoSystemeId: 'fondamentale' in etape && isNotNullNorUndefined(etape.fondamentale.perimetre?.geojson_origine_geo_systeme_id) ? etape.fondamentale.perimetre.geojson_origine_geo_systeme_id : propsHeritees.perimetre?.geojson_origine_geo_systeme_id, + geojson4326Perimetre: + 'fondamentale' in etape && isNotNullNorUndefined(etape.fondamentale.perimetre?.geojson4326_perimetre) + ? etape.fondamentale.perimetre.geojson4326_perimetre + : propsHeritees.perimetre?.geojson4326_perimetre, + geojsonOriginePerimetre: + 'fondamentale' in etape && isNotNullNorUndefined(etape.fondamentale.perimetre?.geojson_origine_perimetre) + ? etape.fondamentale.perimetre.geojson_origine_perimetre + : propsHeritees.perimetre?.geojson_origine_perimetre, + geojsonOrigineGeoSystemeId: + 'fondamentale' in etape && isNotNullNorUndefined(etape.fondamentale.perimetre?.geojson_origine_geo_systeme_id) + ? etape.fondamentale.perimetre.geojson_origine_geo_systeme_id + : propsHeritees.perimetre?.geojson_origine_geo_systeme_id, }, userSuper, titre.id @@ -222,8 +237,8 @@ const fixProlongation = (pool: Pool, checkedProlongation: CheckedProlongation): }) ), Effect.map(() => null), - Effect.catchAll((error) => { - console.log(error) + Effect.catchAll(error => { + console.info(error) return Effect.succeed(null) }) ) @@ -256,7 +271,7 @@ export const checkProlongations = (pool: Pool): Effect.Effect<CheckedProlongatio // const link = isNotNullNorUndefined(checkedProlongation.titre_slug) // ? `http://localhost:4180/titres/${checkedProlongation.titre_slug}` // : `http://localhost:4180/demarches/${checkedProlongation.demarche_id}` - // console.log(`${link} : ${checkedProlongation.errors.join(' | ')}`) + // console.info(`${link} : ${checkedProlongation.errors.join(' | ')}`) // } return [...acc, checkedProlongation] @@ -271,7 +286,7 @@ export const checkProlongations = (pool: Pool): Effect.Effect<CheckedProlongatio const prolongationsAvecDureeManquante = prolongations.filter(({ errors }) => errors.some(error => error.includes('la durée est obligatoire'))) const prolongationsParType = prolongationsEnVrac.reduce<{ [key: string]: number }>((acc, prolongation) => { - const domaine = isNotNullNorUndefined(prolongation.titre_slug) ? `${prolongation.titre_slug.slice(2, 4)}${ prolongation.titre_slug.slice(0, 1)}` : 'inconnu' + const domaine = isNotNullNorUndefined(prolongation.titre_slug) ? `${prolongation.titre_slug.slice(2, 4)}${prolongation.titre_slug.slice(0, 1)}` : 'inconnu' if (isNullOrUndefined(acc[domaine])) { acc[domaine] = 1 } else { @@ -280,9 +295,9 @@ export const checkProlongations = (pool: Pool): Effect.Effect<CheckedProlongatio return acc }, {}) - console.log(`Prolongations en vrac : ${prolongationsEnVrac.length} (${toPercentage(prolongationsEnVrac.length / prolongations.length)})`) - console.log(JSON.stringify(prolongationsParType, null, 2)) - console.log(`Prolongations sans durée : ${prolongationsAvecDureeManquante.length} (${toPercentage(prolongationsAvecDureeManquante.length / prolongationsEnVrac.length)})`) + console.info(`Prolongations en vrac : ${prolongationsEnVrac.length} (${toPercentage(prolongationsEnVrac.length / prolongations.length)})`) + console.info(JSON.stringify(prolongationsParType, null, 2)) + console.info(`Prolongations sans durée : ${prolongationsAvecDureeManquante.length} (${toPercentage(prolongationsAvecDureeManquante.length / prolongationsEnVrac.length)})`) }) ) } -- GitLab