diff --git a/packages/api/src/api/rest/perimetre.queries.ts b/packages/api/src/api/rest/perimetre.queries.ts index a76f150782aaf4f7c9ce49a2f44f23ed4f7a3e79..5e0b3ebeb8706659c7fdcf6ad55db3e4a63b1bd7 100644 --- a/packages/api/src/api/rest/perimetre.queries.ts +++ b/packages/api/src/api/rest/perimetre.queries.ts @@ -14,7 +14,7 @@ import { foretIdValidator } from 'camino-common/src/static/forets' import { sdomZoneIdValidator } from 'camino-common/src/static/sdom' import { KM2, M2, km2Validator, m2Validator } from 'camino-common/src/number' import { isNullOrUndefined, onlyUnique } from 'camino-common/src/typescript-tools' -import { ZodUnparseable, zodParseEffect, zodParseEffectCallback } from '../../tools/fp-tools' +import { zodParseEffect, zodParseEffectTyped, ZodUnparseable } from '../../tools/fp-tools' import { CaminoError } from 'camino-common/src/zod-tools' import { Effect, pipe } from 'effect' import { departementIdValidator, toDepartementId } from 'camino-common/src/static/departement' @@ -58,14 +58,7 @@ export const convertPoints = <T extends z.ZodTypeAny>( coordinates => coordinates.length === geojsonPoints.features.length, () => ({ message: convertPointsInvalidNumberOfFeaturesError }) ), - Effect.flatMap((result: [number, number][]) => { - const check = zodParseEffect(arrayTuple4326CoordinateValidator, result) - - return Effect.matchEffect(check, { - onSuccess: () => Effect.succeed(result), - onFailure: error => Effect.fail({ ...error, message: invalidSridError, detail: 'Vérifiez que le géosystème correspond bien à celui du fichier' }), - }) - }), + Effect.tap((result: [number, number][]) => zodParseEffectTyped(arrayTuple4326CoordinateValidator, result, invalidSridError, 'Vérifiez que le géosystème correspond bien à celui du fichier')), Effect.map(coordinates => { return { type: 'FeatureCollection', @@ -87,14 +80,14 @@ const perimetreInvalideError = "Le périmètre n'est pas valide dans le référe const conversionGeometrieError = 'Impossible de convertir la géométrie en JSON' as const const getGeojsonByGeoSystemeIdValidator = z.object({ geojson: multiPolygonValidator }) const polygon4326CoordinatesValidator = z.array(z.array(arrayTuple4326CoordinateValidator.min(3)).min(1)).min(1) - +const transformationImpossible = 'Impossible de transformer le geojson dans le référentiel donné' as const export type GetGeojsonByGeoSystemeIdErrorMessages = | EffectDbQueryAndValidateErrors - | ZodUnparseable | typeof conversionSystemeError | typeof perimetreInvalideError | typeof conversionGeometrieError | typeof invalidSridError + | typeof transformationImpossible export const getGeojsonByGeoSystemeId = ( pool: Pool, fromGeoSystemeId: GeoSystemeId, @@ -117,16 +110,7 @@ export const getGeojsonByGeoSystemeId = ( result => result.length === 1, () => ({ message: conversionSystemeError, extra: to4326GeoSystemeId }) ), - Effect.flatMap(result => { - const coordinates: [number, number][][][] = result[0].geojson.coordinates - - const check = zodParseEffect(polygon4326CoordinatesValidator, coordinates) - - return Effect.matchEffect(check, { - onSuccess: () => Effect.succeed(result), - onFailure: error => Effect.fail({ ...error, message: invalidSridError, detail: 'Vérifiez que le géosystème correspond bien à celui du fichier' }), - }) - }), + Effect.tap(result => zodParseEffectTyped(polygon4326CoordinatesValidator, result[0].geojson.coordinates, invalidSridError, 'Vérifiez que le géosystème correspond bien à celui du fichier')), Effect.map(result => { if (fromGeoSystemeId === to4326GeoSystemeId) { return geojson @@ -139,7 +123,7 @@ export const getGeojsonByGeoSystemeId = ( return feature }), - Effect.flatMap(zodParseEffectCallback(featureMultiPolygonValidator)) + Effect.flatMap(result => zodParseEffectTyped(featureMultiPolygonValidator, result, transformationImpossible)) ) } diff --git a/packages/api/src/api/rest/perimetre.ts b/packages/api/src/api/rest/perimetre.ts index 8015b9c44813d7fe7a049b8430d39016082e7632..f51a56eda1691ac433755dfe5ada0476cc104af3 100644 --- a/packages/api/src/api/rest/perimetre.ts +++ b/packages/api/src/api/rest/perimetre.ts @@ -415,6 +415,7 @@ export const geojsonImport: RestNewPostCall<'/rest/geojson/import/:geoSystemeId' 'Une erreur inattendue est survenue lors de la récupération des informations geojson en base', "Impossible d'exécuter la requête dans la base de données", 'Les données en base ne correspondent pas à ce qui est attendu', + 'Impossible de transformer le geojson dans le référentiel donné', () => ({ ...caminoError, status: HTTP_STATUS.INTERNAL_SERVER_ERROR }) ), Match.whenOr( diff --git a/packages/api/src/api/rest/titres.ts b/packages/api/src/api/rest/titres.ts index 709ef81dbc7e6f614967c647d291a499ce0d0d33..3f31cf708289ffeba701ef26c4ad20d61d3c60be 100644 --- a/packages/api/src/api/rest/titres.ts +++ b/packages/api/src/api/rest/titres.ts @@ -155,10 +155,17 @@ export const titresAdministrations = derniereEtape = etapesDerniereDemarche[etapesDerniereDemarche.length - 1] if (isNotNullNorUndefined(machine)) { try { - enAttenteDeAdministration = machine.whoIsBlocking(etapesDerniereDemarche).includes(user.administrationId) + const whoIsBlocking = machine.whoIsBlocking(etapesDerniereDemarche) + if (!whoIsBlocking.valid) { + throw new Error(whoIsBlocking.error) + } + enAttenteDeAdministration = whoIsBlocking.value.includes(user.administrationId) const nextEtapes = machine.possibleNextEtapes(etapesDerniereDemarche, getCurrent()) + if (!nextEtapes.valid) { + throw new Error(nextEtapes.error) + } prochainesEtapes.push( - ...nextEtapes + ...nextEtapes.value .map(etape => etape.etapeTypeId) .filter(onlyUnique) .filter(etape => !etapesAMasquer.includes(etape)) diff --git a/packages/api/src/business/rules-demarches/axm/oct.machine.test.ts b/packages/api/src/business/rules-demarches/axm/oct.machine.test.ts index 616a7a027f54fc3a5c87963f7db8ea64e1475bf0..03fe9462846cdf1f9f609cefb17bac7f7ced3410 100644 --- a/packages/api/src/business/rules-demarches/axm/oct.machine.test.ts +++ b/packages/api/src/business/rules-demarches/axm/oct.machine.test.ts @@ -19,7 +19,7 @@ describe('vérifie l’arbre d’octroi d’AXM', () => { "ENREGISTRER_DEMANDE (confidentielle, déposé ) -> [DEMANDER_COMPLEMENTS_POUR_RECEVABILITE,FAIRE_CLASSEMENT_SANS_SUITE,FAIRE_DESISTEMENT_DEMANDEUR,FAIRE_RECEVABILITE_DEMANDE_DEFAVORABLE,FAIRE_RECEVABILITE_DEMANDE_FAVORABLE,RENDRE_DECISION_IMPLICITE_REJET]", ] `) - expect(axmOctMachine.whoIsBlocking(etapes)).toStrictEqual([ADMINISTRATION_IDS['DGTM - GUYANE']]) + expect(axmOctMachine.whoIsBlocking(etapes)).toStrictEqual({ valid: true, value: [ADMINISTRATION_IDS['DGTM - GUYANE']] }) }) test('peut faire l’avis du DREAL sans aucun autre avis 30 jours après la saisine des services', () => { @@ -42,9 +42,13 @@ describe('vérifie l’arbre d’octroi d’AXM', () => { "RENDRE_AVIS_DREAL (publique , en instruction ) -> [FAIRE_CLASSEMENT_SANS_SUITE,FAIRE_DESISTEMENT_DEMANDEUR,FAIRE_SAISINE_COMMISSION_DEPARTEMENTALE_DES_MINES,RENDRE_AVIS_COMMISSION_DEPARTEMENTALE_DES_MINES,RENDRE_AVIS_COMMISSION_DEPARTEMENTALE_DES_MINES_AJOURNE]", ] `) + const possibleNextEtapes = machine.possibleNextEtapes(etapes, toCaminoDate('2022-06-15')) + expect(possibleNextEtapes.valid).toBe(true) + if (!possibleNextEtapes.valid) { + throw new Error(possibleNextEtapes.error) + } expect( - machine - .possibleNextEtapes(etapes, toCaminoDate('2022-06-15')) + possibleNextEtapes.value .map(({ type }) => type) .filter(onlyUnique) .toSorted() @@ -134,7 +138,7 @@ describe('vérifie l’arbre d’octroi d’AXM', () => { "FAIRE_CLASSEMENT_SANS_SUITE (confidentielle, classé sans suite ) -> []", ] `) - expect(machine.whoIsBlocking(etapes)).toStrictEqual([]) + expect(machine.whoIsBlocking(etapes)).toStrictEqual({ valid: true, value: [] }) }) test('ne peut pas faire deux fois la même étape à la même date', () => { @@ -204,9 +208,13 @@ describe('vérifie l’arbre d’octroi d’AXM', () => { ETES.avisDesCollectivites.FAIT, ETES.avisDesServicesEtCommissionsConsultatives.FAIT, ]) + let possibleNextEtapes = machine.possibleNextEtapes(etapes, toCaminoDate('2022-05-06')) + expect(possibleNextEtapes.valid).toBe(true) + if (!possibleNextEtapes.valid) { + throw new Error(possibleNextEtapes.error) + } expect( - machine - .possibleNextEtapes(etapes, toCaminoDate('2022-05-06')) + possibleNextEtapes.value .map(({ type }) => type) .filter(onlyUnique) .toSorted() @@ -218,9 +226,14 @@ describe('vérifie l’arbre d’octroi d’AXM', () => { "RENDRE_AVIS_DREAL", ] `) + + possibleNextEtapes = machine.possibleNextEtapes(etapes, toCaminoDate('2022-05-05')) + expect(possibleNextEtapes.valid).toBe(true) + if (!possibleNextEtapes.valid) { + throw new Error(possibleNextEtapes.error) + } expect( - machine - .possibleNextEtapes(etapes, toCaminoDate('2022-05-05')) + possibleNextEtapes.value .map(({ type }) => type) .filter(onlyUnique) .toSorted() diff --git a/packages/api/src/business/rules-demarches/machine-helper.test.ts b/packages/api/src/business/rules-demarches/machine-helper.test.ts index 671000eda3634ff2ee95744c2b299a77306e025c..da8b1a570b4ecda0eaae9e31fe9e4cee79f2d5ec 100644 --- a/packages/api/src/business/rules-demarches/machine-helper.test.ts +++ b/packages/api/src/business/rules-demarches/machine-helper.test.ts @@ -364,7 +364,7 @@ describe('whoIsBlocking', () => { date: toCaminoDate('2021-02-03'), }, ]) - ).toStrictEqual([ADMINISTRATION_IDS['DGTM - GUYANE']]) + ).toStrictEqual({ valid: true, value: [ADMINISTRATION_IDS['DGTM - GUYANE']] }) }) test('on attend la DGTM pour la validation du paiement des frais de dossier', () => { @@ -391,7 +391,7 @@ describe('whoIsBlocking', () => { date: toCaminoDate('2021-02-04'), }, ]) - ).toStrictEqual([ADMINISTRATION_IDS['DGTM - GUYANE']]) + ).toStrictEqual({ valid: true, value: [ADMINISTRATION_IDS['DGTM - GUYANE']] }) }) test('on attend personne', () => { @@ -428,7 +428,7 @@ describe('whoIsBlocking', () => { date: toCaminoDate('2021-02-06'), }, ]) - ).toStrictEqual([]) + ).toStrictEqual({ valid: true, value: [] }) }) }) @@ -451,36 +451,39 @@ describe('mainStep', () => { toCaminoDate('2021-02-03') ) ).toMatchInlineSnapshot(` - [ - { - "contenu": undefined, - "etapeStatutId": "fai", - "etapeTypeId": "css", - "mainStep": false, - "type": "CLASSER_SANS_SUITE", - }, - { - "contenu": undefined, - "etapeStatutId": "fai", - "etapeTypeId": "des", - "mainStep": false, - "type": "DESISTER_PAR_LE_DEMANDEUR", - }, - { - "contenu": undefined, - "etapeStatutId": "fai", - "etapeTypeId": "mod", - "mainStep": false, - "type": "MODIFIER_DEMANDE", - }, - { - "contenu": undefined, - "etapeStatutId": "fai", - "etapeTypeId": "pfd", - "mainStep": true, - "type": "PAYER_FRAIS_DE_DOSSIER", - }, - ] + { + "valid": true, + "value": [ + { + "contenu": undefined, + "etapeStatutId": "fai", + "etapeTypeId": "css", + "mainStep": false, + "type": "CLASSER_SANS_SUITE", + }, + { + "contenu": undefined, + "etapeStatutId": "fai", + "etapeTypeId": "des", + "mainStep": false, + "type": "DESISTER_PAR_LE_DEMANDEUR", + }, + { + "contenu": undefined, + "etapeStatutId": "fai", + "etapeTypeId": "mod", + "mainStep": false, + "type": "MODIFIER_DEMANDE", + }, + { + "contenu": undefined, + "etapeStatutId": "fai", + "etapeTypeId": "pfd", + "mainStep": true, + "type": "PAYER_FRAIS_DE_DOSSIER", + }, + ], + } `) }) test('possibleNextEtapes après une recevabilité favorable on rendre un avis des services et commissions consultatives', () => { @@ -497,50 +500,53 @@ describe('mainStep', () => { toCaminoDate('2021-02-03') ) ).toMatchInlineSnapshot(` - [ - { - "contenu": undefined, - "etapeStatutId": "fai", - "etapeTypeId": "asc", - "mainStep": true, - "type": "RENDRE_AVIS_DES_SERVICES_ET_COMMISSIONS_CONSULTATIVES", - }, - { - "contenu": undefined, - "etapeStatutId": "fai", - "etapeTypeId": "css", - "mainStep": false, - "type": "CLASSER_SANS_SUITE", - }, - { - "contenu": undefined, - "etapeStatutId": "fai", - "etapeTypeId": "des", - "mainStep": false, - "type": "DESISTER_PAR_LE_DEMANDEUR", - }, - { - "contenu": undefined, - "etapeStatutId": "fav", - "etapeTypeId": "exp", - "mainStep": false, - "type": "RECEVOIR_EXPERTISE", - }, - { - "contenu": undefined, - "etapeStatutId": "def", - "etapeTypeId": "exp", - "mainStep": false, - "type": "RECEVOIR_EXPERTISE", - }, - { - "contenu": undefined, - "etapeStatutId": "fai", - "etapeTypeId": "mod", - "mainStep": false, - "type": "MODIFIER_DEMANDE", - }, - ] + { + "valid": true, + "value": [ + { + "contenu": undefined, + "etapeStatutId": "fai", + "etapeTypeId": "asc", + "mainStep": true, + "type": "RENDRE_AVIS_DES_SERVICES_ET_COMMISSIONS_CONSULTATIVES", + }, + { + "contenu": undefined, + "etapeStatutId": "fai", + "etapeTypeId": "css", + "mainStep": false, + "type": "CLASSER_SANS_SUITE", + }, + { + "contenu": undefined, + "etapeStatutId": "fai", + "etapeTypeId": "des", + "mainStep": false, + "type": "DESISTER_PAR_LE_DEMANDEUR", + }, + { + "contenu": undefined, + "etapeStatutId": "fav", + "etapeTypeId": "exp", + "mainStep": false, + "type": "RECEVOIR_EXPERTISE", + }, + { + "contenu": undefined, + "etapeStatutId": "def", + "etapeTypeId": "exp", + "mainStep": false, + "type": "RECEVOIR_EXPERTISE", + }, + { + "contenu": undefined, + "etapeStatutId": "fai", + "etapeTypeId": "mod", + "mainStep": false, + "type": "MODIFIER_DEMANDE", + }, + ], + } `) }) test('après un avis des services et commissions consultatives on doit avoir la saisine de la commission des autorisations de recherches minières', () => { @@ -558,50 +564,53 @@ describe('mainStep', () => { toCaminoDate('2021-02-03') ) ).toMatchInlineSnapshot(` - [ - { - "contenu": undefined, - "etapeStatutId": "fai", - "etapeTypeId": "css", - "mainStep": false, - "type": "CLASSER_SANS_SUITE", - }, - { - "contenu": undefined, - "etapeStatutId": "fai", - "etapeTypeId": "des", - "mainStep": false, - "type": "DESISTER_PAR_LE_DEMANDEUR", - }, - { - "contenu": undefined, - "etapeStatutId": "fav", - "etapeTypeId": "exp", - "mainStep": false, - "type": "RECEVOIR_EXPERTISE", - }, - { - "contenu": undefined, - "etapeStatutId": "def", - "etapeTypeId": "exp", - "mainStep": false, - "type": "RECEVOIR_EXPERTISE", - }, - { - "contenu": undefined, - "etapeStatutId": "fai", - "etapeTypeId": "mod", - "mainStep": false, - "type": "MODIFIER_DEMANDE", - }, - { - "contenu": undefined, - "etapeStatutId": "fai", - "etapeTypeId": "sca", - "mainStep": true, - "type": "FAIRE_SAISINE_CARM", - }, - ] + { + "valid": true, + "value": [ + { + "contenu": undefined, + "etapeStatutId": "fai", + "etapeTypeId": "css", + "mainStep": false, + "type": "CLASSER_SANS_SUITE", + }, + { + "contenu": undefined, + "etapeStatutId": "fai", + "etapeTypeId": "des", + "mainStep": false, + "type": "DESISTER_PAR_LE_DEMANDEUR", + }, + { + "contenu": undefined, + "etapeStatutId": "fav", + "etapeTypeId": "exp", + "mainStep": false, + "type": "RECEVOIR_EXPERTISE", + }, + { + "contenu": undefined, + "etapeStatutId": "def", + "etapeTypeId": "exp", + "mainStep": false, + "type": "RECEVOIR_EXPERTISE", + }, + { + "contenu": undefined, + "etapeStatutId": "fai", + "etapeTypeId": "mod", + "mainStep": false, + "type": "MODIFIER_DEMANDE", + }, + { + "contenu": undefined, + "etapeStatutId": "fai", + "etapeTypeId": "sca", + "mainStep": true, + "type": "FAIRE_SAISINE_CARM", + }, + ], + } `) }) test('après la validation de frais de paiement on doit faire une recevabilité', () => { @@ -617,57 +626,60 @@ describe('mainStep', () => { toCaminoDate('2021-02-03') ) ).toMatchInlineSnapshot(` - [ - { - "contenu": undefined, - "etapeStatutId": "fai", - "etapeTypeId": "css", - "mainStep": false, - "type": "CLASSER_SANS_SUITE", - }, - { - "contenu": undefined, - "etapeStatutId": "fai", - "etapeTypeId": "des", - "mainStep": false, - "type": "DESISTER_PAR_LE_DEMANDEUR", - }, - { - "contenu": undefined, - "etapeStatutId": "fai", - "etapeTypeId": "mca", - "mainStep": false, - "type": "DEMANDER_COMPLEMENTS_MCR", - }, - { - "contenu": undefined, - "etapeStatutId": "def", - "etapeTypeId": "mcr", - "mainStep": false, - "type": "DECLARER_DEMANDE_DEFAVORABLE", - }, - { - "contenu": undefined, - "etapeStatutId": "fav", - "etapeTypeId": "mcr", - "mainStep": true, - "type": "DECLARER_DEMANDE_FAVORABLE", - }, - { - "contenu": undefined, - "etapeStatutId": "fai", - "etapeTypeId": "mim", - "mainStep": false, - "type": "DEMANDER_INFORMATION_MCR", - }, - { - "contenu": undefined, - "etapeStatutId": "fai", - "etapeTypeId": "mod", - "mainStep": false, - "type": "MODIFIER_DEMANDE", - }, - ] + { + "valid": true, + "value": [ + { + "contenu": undefined, + "etapeStatutId": "fai", + "etapeTypeId": "css", + "mainStep": false, + "type": "CLASSER_SANS_SUITE", + }, + { + "contenu": undefined, + "etapeStatutId": "fai", + "etapeTypeId": "des", + "mainStep": false, + "type": "DESISTER_PAR_LE_DEMANDEUR", + }, + { + "contenu": undefined, + "etapeStatutId": "fai", + "etapeTypeId": "mca", + "mainStep": false, + "type": "DEMANDER_COMPLEMENTS_MCR", + }, + { + "contenu": undefined, + "etapeStatutId": "def", + "etapeTypeId": "mcr", + "mainStep": false, + "type": "DECLARER_DEMANDE_DEFAVORABLE", + }, + { + "contenu": undefined, + "etapeStatutId": "fav", + "etapeTypeId": "mcr", + "mainStep": true, + "type": "DECLARER_DEMANDE_FAVORABLE", + }, + { + "contenu": undefined, + "etapeStatutId": "fai", + "etapeTypeId": "mim", + "mainStep": false, + "type": "DEMANDER_INFORMATION_MCR", + }, + { + "contenu": undefined, + "etapeStatutId": "fai", + "etapeTypeId": "mod", + "mainStep": false, + "type": "MODIFIER_DEMANDE", + }, + ], + } `) }) test('après une recevabilité défavorable on doit avoir un avis des services et commissions consultatives', () => { @@ -684,36 +696,39 @@ describe('mainStep', () => { toCaminoDate('2021-02-03') ) ).toMatchInlineSnapshot(` - [ - { - "contenu": undefined, - "etapeStatutId": "fai", - "etapeTypeId": "asc", - "mainStep": true, - "type": "RENDRE_AVIS_DES_SERVICES_ET_COMMISSIONS_CONSULTATIVES", - }, - { - "contenu": undefined, - "etapeStatutId": "fai", - "etapeTypeId": "css", - "mainStep": false, - "type": "CLASSER_SANS_SUITE", - }, - { - "contenu": undefined, - "etapeStatutId": "fai", - "etapeTypeId": "des", - "mainStep": false, - "type": "DESISTER_PAR_LE_DEMANDEUR", - }, - { - "contenu": undefined, - "etapeStatutId": "fai", - "etapeTypeId": "mod", - "mainStep": false, - "type": "MODIFIER_DEMANDE", - }, - ] + { + "valid": true, + "value": [ + { + "contenu": undefined, + "etapeStatutId": "fai", + "etapeTypeId": "asc", + "mainStep": true, + "type": "RENDRE_AVIS_DES_SERVICES_ET_COMMISSIONS_CONSULTATIVES", + }, + { + "contenu": undefined, + "etapeStatutId": "fai", + "etapeTypeId": "css", + "mainStep": false, + "type": "CLASSER_SANS_SUITE", + }, + { + "contenu": undefined, + "etapeStatutId": "fai", + "etapeTypeId": "des", + "mainStep": false, + "type": "DESISTER_PAR_LE_DEMANDEUR", + }, + { + "contenu": undefined, + "etapeStatutId": "fai", + "etapeTypeId": "mod", + "mainStep": false, + "type": "MODIFIER_DEMANDE", + }, + ], + } `) }) }) diff --git a/packages/api/src/business/rules-demarches/machine-helper.ts b/packages/api/src/business/rules-demarches/machine-helper.ts index 9142bd5c2ecc73e5d00a122b05e9cdb0e88a8b4d..c4c604acb1890f5700eaac3aa22f87ae8f5a3e2c 100644 --- a/packages/api/src/business/rules-demarches/machine-helper.ts +++ b/packages/api/src/business/rules-demarches/machine-helper.ts @@ -186,7 +186,7 @@ export abstract class CaminoMachine<CaminoContext extends CaminoCommonContext, C } private goTo(etapes: readonly Etape[]): - | { valid: false; etapeIndex: number } + | { valid: false; etapeIndex: number; error: string } | { valid: true state: CaminoState<CaminoContext, CaminoEvent> @@ -208,7 +208,7 @@ export abstract class CaminoMachine<CaminoContext extends CaminoCommonContext, C if (!service.getSnapshot().can(event) || service.getSnapshot().status === 'done') { service.stop() - return { valid: false, etapeIndex: i } + return { valid: false, etapeIndex: i, error: `Les étapes '${JSON.stringify(etapes)}' sont invalides à partir de l’étape ${i}` } } service.send(event) } @@ -286,21 +286,20 @@ export abstract class CaminoMachine<CaminoContext extends CaminoCommonContext, C } } - private assertGoTo(etapes: readonly Etape[]): CaminoState<CaminoContext, CaminoEvent> { - const value = this.goTo(etapes) - if (!value.valid) { - throw new Error(`Les étapes '${JSON.stringify(etapes)}' sont invalides à partir de l’étape ${value.etapeIndex}`) - } else { - return value.state + public whoIsBlocking(etapes: readonly Etape[]): + | { valid: false; etapeIndex: number; error: string } + | { + valid: true + value: Intervenant[] + } { + const state = this.goTo(etapes) + if (!state.valid) { + return state } - } - - public whoIsBlocking(etapes: readonly Etape[]): Intervenant[] { - const state = this.assertGoTo(etapes) - const responsables: string[] = [...state.tags] + const responsables: string[] = [...state.state.tags] - return intervenants.filter(r => responsables.includes(tags.responsable[r])) + return { valid: true, value: intervenants.filter(r => responsables.includes(tags.responsable[r])) } } // visibleForTesting @@ -318,17 +317,29 @@ export abstract class CaminoMachine<CaminoContext extends CaminoCommonContext, C .toSorted((a, b) => a.type.localeCompare(b.type)) } - public possibleNextEtapes(etapes: readonly Etape[], date: CaminoDate): (OmitDistributive<Etape, 'date' | 'titreTypeId' | 'demarcheTypeId'> & { mainStep: boolean; type: CaminoEvent['type'] })[] { - const state = this.assertGoTo(etapes) + public possibleNextEtapes( + etapes: readonly Etape[], + date: CaminoDate + ): + | { valid: false; etapeIndex: number; error: string } + | { valid: true; value: (OmitDistributive<Etape, 'date' | 'titreTypeId' | 'demarcheTypeId'> & { mainStep: boolean; type: CaminoEvent['type'] })[] } { + const state = this.goTo(etapes) + + if (!state.valid) { + return state + } if (isNotNullNorUndefined(state)) { - return this.possibleNextEvents(state, date) - .flatMap(this.caminoXStateEventToEtapes.bind(this)) - .filter(isNotNullNorUndefined) - .toSorted((a, b) => a.etapeTypeId.localeCompare(b.etapeTypeId)) + return { + valid: true, + value: this.possibleNextEvents(state.state, date) + .flatMap(this.caminoXStateEventToEtapes.bind(this)) + .filter(isNotNullNorUndefined) + .toSorted((a, b) => a.etapeTypeId.localeCompare(b.etapeTypeId)), + } } - return [] + return { valid: true, value: [] } } } 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 cfa13d081b73d345b2f1955ac2fdf23d79e6328e..9ef75833732dda6f86381d0888d42c50b1e4cfa2 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 @@ -676,7 +676,7 @@ describe('vérifie l’arbre des procédures spécifique', () => { ETES.decisionDeLAutoriteAdministrative.REJETE_DECISION_IMPLICITE, ]) - expect(machine.possibleNextEtapes(etapes, dateFin)).toStrictEqual([]) + expect(machine.possibleNextEtapes(etapes, dateFin)).toStrictEqual({ valid: true, value: [] }) expect(machine.demarcheStatut(etapes)).toMatchInlineSnapshot(` { "demarcheDateDebut": { @@ -710,7 +710,7 @@ describe('vérifie l’arbre des procédures spécifique', () => { ETES.decisionDeLAutoriteAdministrative.REJETE_DECISION_IMPLICITE, ]) - expect(machine.possibleNextEtapes(etapes, dateFin)).toStrictEqual([]) + expect(machine.possibleNextEtapes(etapes, dateFin)).toStrictEqual({ valid: true, value: [] }) expect(machine.demarcheStatut(etapes)).toMatchInlineSnapshot(` { "demarcheDateDebut": { diff --git a/packages/api/src/business/validations/titre-demarche-etat-validate.test.ts b/packages/api/src/business/validations/titre-demarche-etat-validate.test.ts index 16e21079bfc5e49926cebe204e6d4b0d38fd0fbe..5d4d48f48bead60b7d1ab1db0ba16691fdaa69c0 100644 --- a/packages/api/src/business/validations/titre-demarche-etat-validate.test.ts +++ b/packages/api/src/business/validations/titre-demarche-etat-validate.test.ts @@ -806,6 +806,230 @@ describe('getPossiblesEtapesTypes', () => { `) }) + test("peut déplacer la saisineDuPrefet qui est le même jour que d'autres", () => { + const sppId = newEtapeId('spp') + const etapes: TitreEtapeForMachine[] = [ + { + typeId: ETAPES_TYPES.demande, + date: toCaminoDate('2023-02-28'), + isBrouillon: ETAPE_IS_NOT_BROUILLON, + id: newEtapeId('demandeId'), + ordre: 1, + statutId: 'fai', + communes: [{ id: toCommuneId('64012') }], + demarcheIdsConsentement: [], + }, + { + typeId: ETAPES_TYPES.avisDeMiseEnConcurrenceAuJORF, + date: toCaminoDate('2023-05-03'), + isBrouillon: ETAPE_IS_NOT_BROUILLON, + id: newEtapeId('anf'), + ordre: 5, + statutId: 'ter', + communes: [{ id: toCommuneId('64012') }], + demarcheIdsConsentement: [], + }, + { + typeId: ETAPES_TYPES.enregistrementDeLaDemande, + date: toCaminoDate('2023-05-03'), + isBrouillon: ETAPE_IS_NOT_BROUILLON, + id: newEtapeId('men'), + ordre: 2, + statutId: 'fai', + communes: [], + demarcheIdsConsentement: [], + }, + { + typeId: ETAPES_TYPES.saisineDuPrefet, + date: toCaminoDate('2023-05-03'), + isBrouillon: ETAPE_IS_NOT_BROUILLON, + id: sppId, + ordre: 3, + statutId: 'fai', + communes: [], + demarcheIdsConsentement: [], + }, + { + typeId: ETAPES_TYPES.recevabiliteDeLaDemande, + date: toCaminoDate('2023-05-03'), + isBrouillon: ETAPE_IS_NOT_BROUILLON, + id: newEtapeId('mcr'), + ordre: 4, + statutId: 'fav', + communes: [], + demarcheIdsConsentement: [], + }, + ] + + expect( + getPossiblesEtapesTypes( + new PrmOctMachine(TITRES_TYPES_IDS.PERMIS_EXCLUSIF_DE_RECHERCHES_METAUX, DEMARCHES_TYPES_IDS.Octroi), + TITRES_TYPES_IDS.PERMIS_EXCLUSIF_DE_RECHERCHES_METAUX, + DEMARCHES_TYPES_IDS.Octroi, + ETAPES_TYPES.saisineDuPrefet, + sppId, + toCaminoDate('2023-05-03'), + etapes + ) + ).toMatchInlineSnapshot(` + { + "spp": { + "etapeStatutIds": [ + "fai", + ], + "mainStep": true, + }, + } + `) + }) + + test("peut déplacer l'enregistrement de la demande le même jour", () => { + const menId = newEtapeId('menId') + const etapes: TitreEtapeForMachine[] = [ + { + typeId: ETAPES_TYPES.demande, + date: toCaminoDate('2023-02-28'), + isBrouillon: ETAPE_IS_NOT_BROUILLON, + id: newEtapeId('demandeId'), + ordre: 1, + statutId: 'fai', + communes: [{ id: toCommuneId('64012') }], + demarcheIdsConsentement: [], + }, + { + typeId: ETAPES_TYPES.avisDeMiseEnConcurrenceAuJORF, + date: toCaminoDate('2023-05-03'), + isBrouillon: ETAPE_IS_NOT_BROUILLON, + id: newEtapeId('anf'), + ordre: 5, + statutId: 'ter', + communes: [{ id: toCommuneId('64012') }], + demarcheIdsConsentement: [], + }, + { + typeId: ETAPES_TYPES.enregistrementDeLaDemande, + date: toCaminoDate('2023-05-03'), + isBrouillon: ETAPE_IS_NOT_BROUILLON, + id: menId, + ordre: 2, + statutId: 'fai', + communes: [], + demarcheIdsConsentement: [], + }, + { + typeId: ETAPES_TYPES.saisineDuPrefet, + date: toCaminoDate('2023-05-03'), + isBrouillon: ETAPE_IS_NOT_BROUILLON, + id: newEtapeId('spp'), + ordre: 3, + statutId: 'fai', + communes: [], + demarcheIdsConsentement: [], + }, + { + typeId: ETAPES_TYPES.recevabiliteDeLaDemande, + date: toCaminoDate('2023-05-03'), + isBrouillon: ETAPE_IS_NOT_BROUILLON, + id: newEtapeId('mcr'), + ordre: 4, + statutId: 'fav', + communes: [], + demarcheIdsConsentement: [], + }, + ] + + expect( + getPossiblesEtapesTypes( + new PrmOctMachine(TITRES_TYPES_IDS.PERMIS_EXCLUSIF_DE_RECHERCHES_METAUX, DEMARCHES_TYPES_IDS.Octroi), + TITRES_TYPES_IDS.PERMIS_EXCLUSIF_DE_RECHERCHES_METAUX, + DEMARCHES_TYPES_IDS.Octroi, + ETAPES_TYPES.enregistrementDeLaDemande, + menId, + toCaminoDate('2023-05-03'), + etapes + ) + ).toMatchInlineSnapshot(` + { + "men": { + "etapeStatutIds": [ + "fai", + ], + "mainStep": true, + }, + } + `) + }) + + test("ne peut pas déplacer l'enregistrement de la demande après les autres étapes", () => { + const menId = newEtapeId('menId') + const etapes: TitreEtapeForMachine[] = [ + { + typeId: ETAPES_TYPES.demande, + date: toCaminoDate('2023-02-28'), + isBrouillon: ETAPE_IS_NOT_BROUILLON, + id: newEtapeId('demandeId'), + ordre: 1, + statutId: 'fai', + communes: [{ id: toCommuneId('64012') }], + demarcheIdsConsentement: [], + }, + { + typeId: ETAPES_TYPES.avisDeMiseEnConcurrenceAuJORF, + date: toCaminoDate('2023-05-03'), + isBrouillon: ETAPE_IS_NOT_BROUILLON, + id: newEtapeId('anf'), + ordre: 5, + statutId: 'ter', + communes: [{ id: toCommuneId('64012') }], + demarcheIdsConsentement: [], + }, + { + typeId: ETAPES_TYPES.enregistrementDeLaDemande, + date: toCaminoDate('2023-05-03'), + isBrouillon: ETAPE_IS_NOT_BROUILLON, + id: menId, + ordre: 2, + statutId: 'fai', + communes: [], + demarcheIdsConsentement: [], + }, + { + typeId: ETAPES_TYPES.saisineDuPrefet, + date: toCaminoDate('2023-05-03'), + isBrouillon: ETAPE_IS_NOT_BROUILLON, + id: newEtapeId('spp'), + ordre: 3, + statutId: 'fai', + communes: [], + demarcheIdsConsentement: [], + }, + { + typeId: ETAPES_TYPES.recevabiliteDeLaDemande, + date: toCaminoDate('2023-05-03'), + isBrouillon: ETAPE_IS_NOT_BROUILLON, + id: newEtapeId('mcr'), + ordre: 4, + statutId: 'fav', + communes: [], + demarcheIdsConsentement: [], + }, + ] + + expect( + getPossiblesEtapesTypes( + new PrmOctMachine(TITRES_TYPES_IDS.PERMIS_EXCLUSIF_DE_RECHERCHES_METAUX, DEMARCHES_TYPES_IDS.Octroi), + TITRES_TYPES_IDS.PERMIS_EXCLUSIF_DE_RECHERCHES_METAUX, + DEMARCHES_TYPES_IDS.Octroi, + ETAPES_TYPES.enregistrementDeLaDemande, + menId, + toCaminoDate('2023-06-01'), + etapes + ) + ).toMatchInlineSnapshot(` + {} + `) + }) + test('peut créer une étape sur une procédure spécifique vide', () => { expect(getPossiblesEtapesTypes(new ProcedureSpecifiqueMachine('cxm', 'oct'), 'cxm', 'oct', undefined, undefined, toCaminoDate('4000-02-01'), [])).toMatchInlineSnapshot(` { 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 df69a3fec9286bb1bb684e6682509d495f90df80..611b6d24b6f5597b1a3f2ad4d57d571bc26186a8 100644 --- a/packages/api/src/business/validations/titre-demarche-etat-validate.ts +++ b/packages/api/src/business/validations/titre-demarche-etat-validate.ts @@ -192,25 +192,36 @@ export const getPossiblesEtapesTypes = ( const etapesTypesPossibleACetteDateOuALaPlaceDeLEtape = (machine: CaminoMachines, etapes: TitreEtapeForMachine[], titreEtapeId: string | null, date: CaminoDate): EtapeTypeEtapeStatutWithMainStep => { const sortedEtapes = titreEtapesSortAscByOrdre(etapes).filter(etape => etape.id !== titreEtapeId) const etapesAvant: Etape[] = [] + const etapesPendant: Etape[] = [] + const etapesApres: Etape[] = [] - // TODO 2022-07-12: Il faudrait mieux gérer les étapes à la même date que l'étape qu'on veut rajouter - // elles ne sont ni avant, ni après, mais potentiellement au milieu de toutes ces étapes - // UPDATE 2025-03-31: il n'y en a pas en prod - etapesAvant.push(...toMachineEtapes(sortedEtapes.filter(etape => etape.date <= date))) - etapesApres.push(...toMachineEtapes(sortedEtapes.slice(etapesAvant.length))) + etapesAvant.push(...toMachineEtapes(sortedEtapes.filter(etape => etape.date < date))) + etapesPendant.push(...toMachineEtapes(sortedEtapes.filter(etape => etape.date === date))) + etapesApres.push(...toMachineEtapes(sortedEtapes.slice(etapesAvant.length + etapesPendant.length))) - const etapesPossiblesRaw = machine.possibleNextEtapes(etapesAvant, date) + if (!machine.isEtapesOk(etapesAvant)) { + return {} + } const etapesPossibles = [] - for (const et of etapesPossiblesRaw) { - const newEtapes = [...etapesAvant] - const items = { ...et, date } - newEtapes.push(items) - newEtapes.push(...etapesApres) + for (let i = 0; i <= etapesPendant.length; i++) { + const etapeEnCours = [...etapesAvant, ...etapesPendant.slice(0, i)] + const etapesPossiblesRaw = machine.possibleNextEtapes(etapeEnCours, date) + if (etapesPossiblesRaw.valid) { + for (const et of etapesPossiblesRaw.value) { + const newEtapes = [...etapeEnCours] + + const items = { ...et, date } + newEtapes.push(items) + newEtapes.push(...etapesPendant.slice(i)) - if (machine.isEtapesOk(newEtapes)) { - etapesPossibles.push(et) + newEtapes.push(...etapesApres) + + if (machine.isEtapesOk(newEtapes)) { + etapesPossibles.push(et) + } + } } } diff --git a/packages/api/src/tools/fp-tools.ts b/packages/api/src/tools/fp-tools.ts index b670998c55db688ec376adf1c39af32f85ab9c74..73202de0231f6a40f61accd5cadbb9af8a25ff8a 100644 --- a/packages/api/src/tools/fp-tools.ts +++ b/packages/api/src/tools/fp-tools.ts @@ -1,3 +1,4 @@ +import { isNotNullNorUndefined } from 'camino-common/src/typescript-tools' import { CaminoError, CaminoZodErrorReadableMessage, translateIssue } from 'camino-common/src/zod-tools' import { Cause, Effect, Exit, pipe } from 'effect' import { ZodTypeAny } from 'zod' @@ -36,10 +37,10 @@ export const zodParseEffect = <T extends ZodTypeAny>(validator: T, item: unknown }) } -export const zodParseEffectTyped = <T extends ZodTypeAny, U extends string>(validator: T, item: T['_output'], errorMessage: U): Effect.Effect<T['_output'], CaminoError<U>> => { +export const zodParseEffectTyped = <T extends ZodTypeAny, U extends string>(validator: T, item: T['_output'], errorMessage: U, detail?: string): Effect.Effect<T['_output'], CaminoError<U>> => { return Effect.try({ try: () => validator.parse(item), - catch: myError => ({ message: errorMessage, detail: zodErrorToDetail(myError), zodErrorReadableMessage: zodErrorToReadableMessage(myError) }), + catch: myError => ({ message: errorMessage, detail: isNotNullNorUndefined(detail) ? detail : zodErrorToDetail(myError), zodErrorReadableMessage: zodErrorToReadableMessage(myError) }), }) }