Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • pub/pnm-public/camino
1 result
Select Git revision
Show changes
Commits on Source (9)
Showing
with 518 additions and 142 deletions
FROM quay.io/keycloak/keycloak:25.0.5
FROM quay.io/keycloak/keycloak:26.1.0
# Utiliser le makefile, mettre à jour la version à la main
COPY keycloak-franceconnect-6.2.0.jar /opt/keycloak/providers/keycloak-franceconnect-6.2.0.jar
COPY keycloak-franceconnect-7.0.0.jar /opt/keycloak/providers/keycloak-franceconnect-7.0.0.jar
COPY keycloak-theme-for-kc-26-and-above.jar /opt/keycloak/providers/keycloak-theme-for-kc-26-and-above.jar
COPY keycloak-bcrypt-1.6.0.jar /opt/keycloak/providers/keycloak-bcrypt-1.6.0.jar
COPY keycloak_theme/ /opt/keycloak/themes/camino/
......@@ -274,10 +274,10 @@ endif
# TODO 2024-10-21 une fois tout migré sur ecoCompose, il faudra supprimer tout ce qu'il y a dans infra sauf le dossier ecocompose
keycloak/build:
docker build -t caminofr/camino-keycloak:25.0.5 -f Dockerfile.keycloak infra/ecocompose/keycloak/
docker build -t caminofr/camino-keycloak:26.1.0 -f Dockerfile.keycloak infra/ecocompose/keycloak/
keycloak/push:
docker push caminofr/camino-keycloak:25.0.5
docker push caminofr/camino-keycloak:26.1.0
nginx-proxy/build:
docker build -t caminofr/camino-nginx-proxy:1.6.1 -f Dockerfile.nginx-proxy infra/ecocompose/nginx-proxy/
......
......@@ -43,7 +43,7 @@ services:
- ${UI_PORT}:${UI_PORT}
oauth2:
container_name: camino_oauth2
image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0
image: quay.io/oauth2-proxy/oauth2-proxy:v7.8.1
environment:
OAUTH2_PROXY_PROVIDER: 'keycloak-oidc'
OAUTH2_PROXY_CLIENT_ID: ${KEYCLOAK_CLIENT_ID}
......@@ -79,7 +79,7 @@ services:
- ${OAUTH_PORT}:${OAUTH_PORT}
keycloak:
container_name: camino_keycloak
image: quay.io/keycloak/keycloak:25.0.5
image: quay.io/keycloak/keycloak:26.1.0
depends_on:
- db
environment:
......@@ -89,7 +89,8 @@ services:
KC_DB_PASSWORD: "${PGPASSWORD}"
KC_DB_USERNAME: "${PGUSER}"
volumes:
- $PWD/infra/roles/camino/files/keycloak-franceconnect-6.2.0.jar:/opt/keycloak/providers/keycloak-franceconnect-6.2.0.jar
- $PWD/infra/roles/camino/files/keycloak-franceconnect-7.0.0.jar:/opt/keycloak/providers/keycloak-franceconnect-7.0.0.jar
- $PWD/infra/roles/camino/files/keycloak-theme-for-kc-26-and-above.jar:/opt/keycloak/providers/keycloak-theme-for-kc-26-and-above.jar
- $PWD/infra/roles/camino/files/keycloak-bcrypt-1.6.0.jar:/opt/keycloak/providers/keycloak-bcrypt-1.6.0.jar
- $PWD/infra/roles/camino/files/keycloak_theme/:/opt/keycloak/themes/camino/
command:
......
......@@ -85,7 +85,7 @@ services:
- /var/run/docker.sock:/var/run/docker.sock:ro
oauth2:
container_name: camino_oauth2
image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0
image: quay.io/oauth2-proxy/oauth2-proxy:v7.8.1
depends_on:
- ui
- keycloak
......@@ -123,7 +123,7 @@ services:
- nginx-proxy
keycloak:
container_name: camino_keycloak
image: caminofr/camino-keycloak:25.0.5
image: caminofr/camino-keycloak:26.1.0
depends_on:
- db
environment:
......@@ -136,6 +136,8 @@ services:
KC_PROXY: "edge"
VIRTUAL_HOST: ${KEYCLOAK_HOST}
VIRTUAL_PORT: ${KEYCLOAK_PORT}
KC_PROXY_HEADERS: xforwarded
KC_HTTP_ENABLED: true
command: "start"
expose:
- ${KEYCLOAK_PORT}
......
## Intégration avec ProConnect
Nous utilisons la version 26 de keycloak, avec le provider [keycloak-franceconnect](https://github.com/InseeFr/Keycloak-FranceConnect) et le thème [DSFR](https://github.com/codegouvfr/keycloak-theme-dsfr)
Ceci est un mini tutoriel pour configurer proconnect et réussir à récupérer le numéro de siret dans le token, afin d'associer des utilisateurs automatiquement à des entreprises.
Dans l'administration de Keycloak, dans le realm Camino
- Ajouter le provider 'Agent connect' (il devrait être renommé ProConnect un jour)
- alias 'proconnect'
- display name 'ProConnect'
- client id 'le clientid généré par la démarche simplifiée'
- client secret 'le client secret généré par la démarche simplifiée
- environnement AgentConnect 'INTEGRATION_INTERNET' (pour les tests, PRODUCTION_INTERNET pour la prod)
Une fois sauvegardé, de nouvelles options apparaissent.
Pour les scopes, on a mis 'openid profile email siret'
On coche 'trust email'
Pour le First login flow override, on a notre propre flow qui s'appelle 'CaminoIdentityProviderFlow'
Dans les mapper, on ajoute un attribute importer siret, il faut mettre siret partout en gros...
Ensuite il faut aller dans le client (par exemple 'camino-local' pour le développement)
Dans l'onglet Settings, modifier Login theme à "DSFR"
Dans l'onglet client scopes, il faut modifier le 'camino-local-dedicated'
Il faut ajouter un mapper de type "user attribute" (pareil, on met siret partout...)
Et voilà \o/
......@@ -20,4 +20,5 @@ nav:
- 'Base de données': '04-deploiement/03-base-de-donnees.md'
- Fichiers: '04-deploiement/04-fichiers.md'
- Scripts: '04-deploiement/05-utils.md'
- Keycloak: '04-deploiement/06-keycloak.md'
- Storybook: '/storybook'
File added
File added
......@@ -47,10 +47,23 @@
owner: camino
group: users
become: True
- name: Supprime l'ancien jar keycloak france connect
ansible.builtin.file:
path: /srv/www/camino/keycloak-franceconnect-6.2.0.jar
state: absent
become: True
- name: Installe le jar keycloak dsfr
ansible.builtin.copy:
src: keycloak-theme-for-kc-26-and-above.jar
dest: /srv/www/camino/keycloak-theme-for-kc-26-and-above.jar
mode: u=rwx,g=rw,o=r
owner: camino
group: users
become: True
- name: Installe le jar keycloak france connect
ansible.builtin.copy:
src: keycloak-franceconnect-6.2.0.jar
dest: /srv/www/camino/keycloak-franceconnect-6.2.0.jar
src: keycloak-franceconnect-7.0.0.jar
dest: /srv/www/camino/keycloak-franceconnect-7.0.0.jar
mode: u=rwx,g=rw,o=r
owner: camino
group: users
......
import { dbManager } from '../../../tests/db-manager'
import { restNewPutCall } from '../../../tests/_utils/index'
import Titres from '../../database/models/titres'
import { userSuper } from '../../database/user-super'
import { afterAll, beforeEach, beforeAll, describe, test, expect, vi } from 'vitest'
import type { Pool } from 'pg'
import { RestEtapeCreation, defaultHeritageProps } from 'camino-common/src/etape-form'
import { HTTP_STATUS } from 'camino-common/src/http'
import { toCaminoDate } from 'camino-common/src/date'
import { TITRES_TYPES_IDS } from 'camino-common/src/static/titresTypes'
import { newDemarcheId, newEtapeId, newTitreId } from '../../database/models/_format/id-create'
import { insertTitreGraph } from '../../../tests/integration-test-helper'
import { TitresStatutIds } from 'camino-common/src/static/titresStatuts'
import { DEMARCHES_TYPES_IDS } from 'camino-common/src/static/demarchesTypes'
import { DemarchesStatutsIds } from 'camino-common/src/static/demarchesStatuts'
import { ETAPE_IS_BROUILLON } from 'camino-common/src/etape'
import { ETAPES_TYPES } from 'camino-common/src/static/etapesTypes'
import { ETAPES_STATUTS } from 'camino-common/src/static/etapesStatuts'
import { communeIdValidator } from 'camino-common/src/static/communes'
import { km2Validator } from 'camino-common/src/number'
import { SUBSTANCES_FISCALES_IDS } from 'camino-common/src/static/substancesFiscales'
import { entrepriseUpsert } from '../../database/queries/entreprises'
import { entrepriseIdValidator } from 'camino-common/src/entreprise'
import { testBlankUser } from 'camino-common/src/tests-utils'
import { FeatureMultiPolygon } from 'camino-common/src/perimetre'
console.info = vi.fn()
console.error = vi.fn()
console.debug = vi.fn()
console.warn = vi.fn()
let dbPool: Pool
beforeAll(async () => {
const { pool } = await dbManager.populateDb()
dbPool = pool
})
beforeEach(async () => {
await Titres.query().delete()
})
afterAll(async () => {
await dbManager.closeKnex()
})
const entrepriseId1 = entrepriseIdValidator.parse('entreprisepourdepot')
const perimetreSimple: FeatureMultiPolygon = {
type: 'Feature',
properties: {},
geometry: {
type: 'MultiPolygon',
coordinates: [
[
[
[7.252418901, 47.828347504],
[7.251749335, 47.827897983],
[7.252418901, 47.827448465],
[7.253088467, 47.827897983],
[7.252418901, 47.828347504],
],
],
],
},
}
const blankEtapeProps: Pick<
RestEtapeCreation,
| 'etapeDocuments'
| 'duree'
| 'dateDebut'
| 'dateFin'
| 'substances'
| 'geojson4326Perimetre'
| 'geojsonOriginePerimetre'
| 'geojson4326Points'
| 'geojsonOriginePoints'
| 'geojsonOrigineForages'
| 'geojsonOrigineGeoSystemeId'
| 'titulaireIds'
| 'amodiataireIds'
| 'note'
| 'etapeAvis'
| 'entrepriseDocumentIds'
| 'heritageProps'
| 'heritageContenu'
| 'contenu'
> = {
etapeDocuments: [],
duree: 2,
dateDebut: null,
dateFin: null,
substances: [SUBSTANCES_FISCALES_IDS.or],
geojson4326Perimetre: perimetreSimple,
geojsonOriginePerimetre: perimetreSimple,
geojson4326Points: null,
geojsonOriginePoints: null,
geojsonOrigineForages: null,
geojsonOrigineGeoSystemeId: '2154',
titulaireIds: [entrepriseId1],
amodiataireIds: [],
note: { valeur: '', is_avertissement: false },
etapeAvis: [],
entrepriseDocumentIds: [],
heritageProps: defaultHeritageProps,
heritageContenu: {},
contenu: {},
} as const
describe('etapeDeposer', () => {
test('ne peut pas déposer une étape (utilisateur non authentifié)', async () => {
const etapeId = newEtapeId()
const result = await restNewPutCall(dbPool, '/rest/etapes/:etapeId/depot', { etapeId }, undefined, {})
expect(result.statusCode).toBe(HTTP_STATUS.FORBIDDEN)
})
test('peut déposer une demande de Permis exclusif de carrières', async () => {
const titreId = newTitreId('titreId')
const demarcheId = newDemarcheId('demarcheId')
const demandeId = newEtapeId('etapeIdDemande')
await entrepriseUpsert({
id: entrepriseId1,
nom: `${entrepriseId1}`,
archive: false,
})
await insertTitreGraph({
id: titreId,
nom: 'test',
typeId: TITRES_TYPES_IDS.PERMIS_EXCLUSIF_DE_CARRIERES_CARRIERES,
titreStatutId: TitresStatutIds.DemandeInitiale,
propsTitreEtapesIds: {},
demarches: [
{
id: demarcheId,
titreId: titreId,
typeId: DEMARCHES_TYPES_IDS.Octroi,
statutId: DemarchesStatutsIds.EnInstruction,
etapes: [
{
id: demandeId,
date: toCaminoDate('2024-11-01'),
isBrouillon: ETAPE_IS_BROUILLON,
titreDemarcheId: demarcheId,
typeId: ETAPES_TYPES.demande,
statutId: ETAPES_STATUTS.FAIT,
communes: [{ id: communeIdValidator.parse('31200'), surface: 12 }],
surface: km2Validator.parse(12),
substances: [SUBSTANCES_FISCALES_IDS.or],
titulaireIds: [entrepriseId1],
duree: 4,
},
],
},
],
})
const res = await restNewPutCall(
dbPool,
'/rest/etapes',
{},
{ ...testBlankUser, role: 'super' },
{
id: demandeId,
typeId: ETAPES_TYPES.demande,
statutId: ETAPES_STATUTS.FAIT,
titreDemarcheId: demarcheId,
date: toCaminoDate('2024-11-03'),
...blankEtapeProps,
}
)
expect(res.body).toMatchInlineSnapshot(`
{
"id": "etapeIdDemande",
}
`)
const resDepot = await restNewPutCall(dbPool, '/rest/etapes/:etapeId/depot', { etapeId: demandeId }, userSuper, {})
expect(resDepot.body).toMatchInlineSnapshot(`
{
"id": "etapeIdDemande",
}
`)
})
})
......@@ -32,6 +32,8 @@ import {
getDocumentsByEtapeId,
getEntrepriseDocumentIdsByEtapeId,
getEtapeAvisLargeObjectIdsByEtapeId,
GetEtapeAvisLargeObjectIdsByEtapeIdErrors,
GetEtapeDocumentLargeObjectIdsByEtapeIdErrors,
insertEtapeAvis,
InsertEtapeAvisErrors,
insertEtapeDocuments,
......@@ -44,7 +46,6 @@ import {
} from '../../database/queries/titres-etapes.queries'
import { getEtapeDataForEdition, hasTitreFrom } from './etapes.queries'
import { SDOMZoneId } from 'camino-common/src/static/sdom'
import { objectClone } from '../../tools/index'
import { titreEtapeAdministrationsEmailsSend, titreEtapeUtilisateursEmailsSend } from '../graphql/resolvers/_titre-etape-email'
import { ConvertPointsErrors, GetGeojsonInformation, GetGeojsonInformationErrorMessages, convertPoints, getGeojsonInformation } from './perimetre.queries'
import { titreEtapeUpdateTask } from '../../business/titre-etape-update'
......@@ -871,86 +872,138 @@ export const updateEtape: RestNewPutCall<'/rest/etapes'> = (rootPipe): Effect.Ef
)
}
export const deposeEtape =
(pool: Pool) =>
async (req: CaminoRequest, res: CustomResponse<void>): Promise<void> => {
const user = req.auth
const etapeId = etapeIdValidator.safeParse(req.params.etapeId)
if (!etapeId.success) {
res.sendStatus(HTTP_STATUS.BAD_REQUEST)
} else {
try {
const id = etapeId.data
if (!user) {
throw new Error("l'étape n'existe pas")
}
const titreEtape = await titreEtapeGet(id, { fields: { id: {} }, fetchHeritage: true }, user)
if (isNullOrUndefined(titreEtape)) throw new Error("l'étape n'existe pas")
const titreEtapeOld = objectClone(titreEtape)
const titreDemarche = await titreDemarcheGet(
titreEtape.titreDemarcheId,
{
fields: {
titre: { pointsEtape: { id: {} }, titulairesEtape: { id: {} }, amodiatairesEtape: { id: {} } },
const demarcheNonExistante = "La démarche n'existe pas" as const
const slugEtapeNonExistant = "Le slug de l'étape est obligatoire" as const
const titreNonExistant = "Le titre n'est pas chargé" as const
const administrationsLocalesNonChargees = 'Les administrations locales du titre ne sont pas chargées' as const
const titulairesNonExistants = 'Les titulaires du titre ne sont pas chargés' as const
const amodiatairesNonExistants = 'Les amodiataires du titre ne sont pas chargés' as const
const droitsInsuffisantsDeposeEtape = 'Droits insuffisants pour déposer cette étape' as const
const brouillonInterdit = "Cette étape n'est pas un brouillon et ne peut pas être redéposée" as const
const impossibleDeRecupererLEtapeApresMaj = "Impossible de récupérer l'étape après mise à jour" as const
const impossibleDeMajLEtape = "Impossible de mettre à jour l'étape" as const
type DeposeEtapeErrors =
| typeof etapeNonExistante
| typeof demarcheNonExistante
| typeof slugEtapeNonExistant
| typeof titreNonExistant
| typeof administrationsLocalesNonChargees
| typeof titulairesNonExistants
| typeof amodiatairesNonExistants
| typeof droitsInsuffisantsDeposeEtape
| typeof brouillonInterdit
| typeof tachesAnnexes
| typeof impossibleDeRecupererLEtapeApresMaj
| typeof impossibleDeMajLEtape
| typeof envoieMails
| EffectDbQueryAndValidateErrors
| GetGeojsonInformationErrorMessages
| TitreEtapeToFlattenEtapeErrors
| GetEtapeDocumentLargeObjectIdsByEtapeIdErrors
| GetEtapeAvisLargeObjectIdsByEtapeIdErrors
export const deposeEtape: RestNewPutCall<'/rest/etapes/:etapeId/depot'> = (rootPipe): Effect.Effect<{ id: EtapeId }, CaminoApiError<DeposeEtapeErrors>> => {
return rootPipe.pipe(
Effect.bind('titreEtape', ({ params, user }) =>
Effect.tryPromise({
try: () => titreEtapeGet(params.etapeId, { fields: { id: {} }, fetchHeritage: true }, user),
catch: e => ({ message: etapeNonExistante, extra: e }),
}).pipe(
Effect.filterOrFail(
(titreEtape: ITitreEtape | null): titreEtape is ITitreEtape => isNotNullNorUndefined(titreEtape),
() => ({ message: etapeNonExistante })
),
Effect.filterOrFail(
titreEtape => isNotNullNorUndefined(titreEtape.slug),
() => ({ message: slugEtapeNonExistant })
)
)
),
Effect.bind('titreDemarche', ({ titreEtape }) =>
Effect.tryPromise({
try: () =>
titreDemarcheGet(
titreEtape.titreDemarcheId,
{
fields: {
titre: { pointsEtape: { id: {} }, titulairesEtape: { id: {} }, amodiatairesEtape: { id: {} } },
},
},
},
userSuper
userSuper
),
catch: e => ({ message: demarcheNonExistante, extra: e }),
}).pipe(
Effect.filterOrFail(
(result: ITitreDemarche | undefined): result is ITitreDemarche => isNotNullNorUndefined(result),
() => ({ message: demarcheNonExistante })
)
if (!titreDemarche) throw new Error("la démarche n'existe pas")
const titre = titreDemarche.titre
if (isNullOrUndefined(titre)) throw new Error("le titre n'est pas chargé")
if (isNullOrUndefined(titre.administrationsLocales)) throw new Error('les administrations locales du titre ne sont pas chargées')
if (isNullOrUndefined(titre.titulaireIds)) throw new Error('les titulaires du titre ne sont pas chargés')
if (isNullOrUndefined(titre.amodiataireIds)) throw new Error('les amodiataires du titre ne sont pas chargés')
if (isNullOrUndefined(titreEtape.slug)) throw new Error("le slug de l'étape est obligatoire")
const sdomZones: SDOMZoneId[] = []
const communes: CommuneId[] = []
if (isNotNullNorUndefined(titreEtape.geojson4326Perimetre)) {
const { sdom, communes: communeFromGeoJson } = await callAndExit(getGeojsonInformation(pool, titreEtape.geojson4326Perimetre.geometry))
communes.push(...communeFromGeoJson.map(({ id }) => id))
sdomZones.push(...sdom)
}
const titreTypeId = memoize(() => Promise.resolve(titre.typeId))
const administrationsLocales = memoize(() => Promise.resolve(titre.administrationsLocales ?? []))
const entreprisesTitulairesOuAmodiataires = memoize(() => {
return Promise.resolve([...(titre.titulaireIds ?? []), ...(titre.amodiataireIds ?? [])])
})
const etapeDocuments = await callAndExit(
getDocumentsByEtapeId(id, pool, user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires, titreEtape.typeId, {
demarche_type_id: titreDemarche.typeId,
entreprises_lecture: titreDemarche.entreprisesLecture ?? false,
public_lecture: titreDemarche.publicLecture ?? false,
titre_public_lecture: titre.publicLecture ?? false,
})
)
),
Effect.bind('titre', ({ titreDemarche }) =>
Effect.succeed(titreDemarche.titre).pipe(
Effect.filterOrFail(
titre => isNotNullNorUndefined(titre),
() => ({ message: titreNonExistant })
),
Effect.filterOrFail(
titre => isNotNullNorUndefined(titre.administrationsLocales),
() => ({ message: administrationsLocalesNonChargees })
),
Effect.filterOrFail(
titre => isNotNullNorUndefined(titre.titulaireIds),
() => ({ message: titulairesNonExistants })
),
Effect.filterOrFail(
titre => isNotNullNorUndefined(titre.amodiataireIds),
() => ({ message: amodiatairesNonExistants })
)
const entrepriseDocuments = await callAndExit(getEntrepriseDocumentIdsByEtapeId({ titre_etape_id: titreEtape.id }, pool, userSuper))
// On utilise le userSuper pour charger tous les avis, car celui qui dépose ne peut peut-être pas voir tous les avis
const etapeAvis = await callAndExit(
getEtapeAvisLargeObjectIdsByEtapeId(id, pool, userSuper, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires, titreEtape.typeId, {
demarche_type_id: titreDemarche.typeId,
entreprises_lecture: titreDemarche.entreprisesLecture ?? false,
public_lecture: titreDemarche.publicLecture ?? false,
titre_public_lecture: titre.publicLecture ?? false,
})
)
),
Effect.let('titreProps', ({ titre }) => ({
titreTypeId: memoize(() => Promise.resolve(titre.typeId)),
administrationsLocales: memoize(() => Promise.resolve(titre.administrationsLocales ?? [])),
entreprisesTitulairesOuAmodiataires: memoize(() => Promise.resolve([...(titre.titulaireIds ?? []), ...(titre.amodiataireIds ?? [])])),
})),
Effect.bind('etapeDocuments', ({ pool, user, titre, titreEtape, titreDemarche, titreProps }) => {
return getDocumentsByEtapeId(titreEtape.id, pool, user, titreProps.titreTypeId, titreProps.administrationsLocales, titreProps.entreprisesTitulairesOuAmodiataires, titreEtape.typeId, {
demarche_type_id: titreDemarche.typeId,
entreprises_lecture: titreDemarche.entreprisesLecture ?? false,
public_lecture: titreDemarche.publicLecture ?? false,
titre_public_lecture: titre.publicLecture ?? false,
})
}),
Effect.bind('entrepriseDocuments', ({ pool, titreEtape }) => getEntrepriseDocumentIdsByEtapeId({ titre_etape_id: titreEtape.id }, pool, userSuper)),
Effect.bind('etapeAvis', ({ pool, titreEtape, titreDemarche, titreProps, titre }) =>
getEtapeAvisLargeObjectIdsByEtapeId(
titreEtape.id,
pool,
userSuper,
titreProps.titreTypeId,
titreProps.administrationsLocales,
titreProps.entreprisesTitulairesOuAmodiataires,
titreEtape.typeId,
{
demarche_type_id: titreDemarche.typeId,
entreprises_lecture: titreDemarche.entreprisesLecture ?? false,
public_lecture: titreDemarche.publicLecture ?? false,
titre_public_lecture: titre.publicLecture ?? false,
}
)
),
Effect.bind('flattenEtape', ({ titreEtape }) => iTitreEtapeToFlattenEtape(titreEtape)),
Effect.let('firstEtapeDate', ({ titreDemarche }) => demarcheEnregistrementDemandeDateFind(titreDemarche.etapes)),
Effect.let('date', ({ user, titreEtape }) => (isEntreprise(user) || isBureauDEtudes(user) ? getCurrent() : titreEtape.date)),
Effect.bind('sdomEtCommunes', ({ titreEtape, pool }) => {
if (isNotNullNorUndefined(titreEtape.geojson4326Perimetre)) {
return getGeojsonInformation(pool, titreEtape.geojson4326Perimetre.geometry).pipe(
Effect.map(({ sdom, communes: communesFromGeoJson }) => ({ sdomZones: sdom, communes: communesFromGeoJson.map(({ id }) => id) }))
)
// TODO 2023-06-14 TS 5.1 n’arrive pas réduire le type de titre
const flattenEtape = await callAndExit(iTitreEtapeToFlattenEtape(titreEtape))
const firstEtapeDate = demarcheEnregistrementDemandeDateFind(titreDemarche.etapes)
const date = isEntreprise(user) || isBureauDEtudes(user) ? getCurrent() : titreEtape.date
const deposable = canDeposeEtape(
}
const emptyResult: { sdomZones: SDOMZoneId[]; communes: CommuneId[] } = { sdomZones: [], communes: [] }
return Effect.succeed(emptyResult)
}),
Effect.filterOrFail(
({ user, titre, titreDemarche, flattenEtape, etapeDocuments, entrepriseDocuments, sdomEtCommunes, etapeAvis, firstEtapeDate, date }) =>
canDeposeEtape(
user,
{ ...titre, titulaires: titre.titulaireIds ?? [], administrationsLocales: titre.administrationsLocales ?? [] },
titreDemarche.id,
......@@ -958,45 +1011,96 @@ export const deposeEtape =
flattenEtape,
etapeDocuments,
entrepriseDocuments,
sdomZones,
communes,
sdomEtCommunes.sdomZones,
sdomEtCommunes.communes,
etapeAvis,
isNotNullNorUndefined(firstEtapeDate) ? firstEtapeDate : firstEtapeDateValidator.parse(date)
),
() => ({ message: droitsInsuffisantsDeposeEtape })
),
Effect.filterOrFail(
({ titreEtape }) => canBeBrouillon(titreEtape.typeId) === ETAPE_IS_BROUILLON,
() => ({ message: brouillonInterdit })
),
Effect.tap(({ titreEtape, date, user, titreDemarche }) =>
Effect.tryPromise({
try: () =>
titreEtapeUpdate(
titreEtape.id,
{
date,
isBrouillon: ETAPE_IS_NOT_BROUILLON,
},
user,
titreDemarche.titreId
),
catch: e => ({ message: impossibleDeMajLEtape, extra: e }),
})
),
Effect.bind('etapeUpdated', ({ titreEtape, user }) =>
Effect.tryPromise({
try: () =>
titreEtapeGet(
titreEtape.id,
{
fields: { id: {} },
},
user
),
catch: e => ({ message: impossibleDeRecupererLEtapeApresMaj, extra: e }),
}).pipe(
Effect.filterOrFail(
etapeUpdated => isNotNullNorUndefined(etapeUpdated),
() => ({ message: impossibleDeRecupererLEtapeApresMaj })
)
if (!deposable) throw new Error('droits insuffisants')
if (canBeBrouillon(titreEtape.typeId) === ETAPE_IS_NOT_BROUILLON) {
throw new Error('cette étape ne peut-être déposée')
}
await titreEtapeUpdate(
titreEtape.id,
{
date,
isBrouillon: ETAPE_IS_NOT_BROUILLON,
},
user,
titreDemarche.titreId
)
const etapeUpdated = await titreEtapeGet(
titreEtape.id,
{
fields: { id: {} },
},
user
)
await titreEtapeUpdateTask(pool, etapeUpdated.id, etapeUpdated.titreDemarcheId, user)
await titreEtapeAdministrationsEmailsSend(etapeUpdated, titreDemarche.typeId, titreDemarche.titreId, titreDemarche.titre!.typeId, user!, titreEtapeOld)
res.sendStatus(HTTP_STATUS.NO_CONTENT)
} catch (e) {
res.sendStatus(HTTP_STATUS.INTERNAL_SERVER_ERROR)
console.error(e)
}
}
}
)
),
Effect.tap(({ pool, etapeUpdated, user }) =>
Effect.tryPromise({
try: () => titreEtapeUpdateTask(pool, etapeUpdated.id, etapeUpdated.titreDemarcheId, user),
catch: e => ({ message: tachesAnnexes, extra: e }),
})
),
Effect.tap(({ etapeUpdated, titreDemarche, user, titreEtape }) =>
Effect.tryPromise({
try: () => titreEtapeAdministrationsEmailsSend(etapeUpdated, titreDemarche.typeId, titreDemarche.titreId, titreDemarche.titre!.typeId, user!, titreEtape),
catch: e => ({ message: envoieMails, extra: e }),
})
),
Effect.map(({ titreEtape }) => ({ id: titreEtape.id })),
Effect.mapError(caminoError =>
Match.value(caminoError.message).pipe(
Match.whenOr('Droits insuffisants pour déposer cette étape', () => ({ ...caminoError, status: HTTP_STATUS.FORBIDDEN })),
Match.whenOr("l'étape n'existe pas", () => ({ ...caminoError, status: HTTP_STATUS.NOT_FOUND })),
Match.whenOr("Cette étape n'est pas un brouillon et ne peut pas être redéposée", () => ({ ...caminoError, status: HTTP_STATUS.BAD_REQUEST })),
Match.whenOr(
"La démarche n'existe pas",
"Le slug de l'étape est obligatoire",
"Le titre n'est pas chargé",
'Les administrations locales du titre ne sont pas chargées',
'Les titulaires du titre ne sont pas chargés',
'Les amodiataires du titre ne sont pas chargés',
"Impossible d'exécuter la requête dans la base de données",
"Impossible de mettre à jour l'étape",
"Impossible de récupérer l'étape après mise à jour",
'Les données en base ne correspondent pas à ce qui est attendu',
'Problème de validation de données',
'Une erreur inattendue est survenue lors de la récupération des informations geojson en base',
"pas d'héritage chargé",
'pas de démarche chargée',
'pas de démarche ou de titre chargé',
'pas de slug',
'une erreur est survenue lors des envois de mail',
'une erreur est survenue lors des tâches annexes',
"une erreur s'est produite lors de la vérification des droits de lecture d'un avis",
"une erreur s'est produite lors de la vérification des droits de lecture d'un document",
() => ({ ...caminoError, status: HTTP_STATUS.INTERNAL_SERVER_ERROR })
),
Match.exhaustive
)
)
)
}
export const getEtapesTypesEtapesStatusWithMainStep: RestNewGetCall<'/rest/etapesTypes/:demarcheId/:date'> = (
rootPipe
......
......@@ -4,7 +4,12 @@ import { User } from 'camino-common/src/roles'
import { DOWNLOAD_FORMATS, DownloadFormat, contentTypes } from 'camino-common/src/rest'
import { Pool } from 'pg'
import { EtapeId, etapeAvisIdValidator, etapeDocumentIdValidator } from 'camino-common/src/etape'
import { getEntrepriseDocumentLargeObjectIdsByEtapeId, getEtapeDocumentLargeObjectIdsByEtapeId, getLargeobjectIdByEtapeAvisId } from '../../database/queries/titres-etapes.queries'
import {
getEntrepriseDocumentLargeObjectIdsByEtapeId,
getEtapeAvisLargeObjectIdsByEtapeId,
getEtapeDocumentLargeObjectIdsByEtapeId,
getLargeobjectIdByEtapeAvisId,
} from '../../database/queries/titres-etapes.queries'
import { LargeObjectManager } from 'pg-large-object'
import express from 'express'
......@@ -14,6 +19,8 @@ import { slugify } from 'camino-common/src/strings'
import { getEtapeDataForEdition, getLargeobjectIdByEtapeDocumentId } from './etapes.queries'
import { EtapesTypes } from 'camino-common/src/static/etapesTypes'
import { callAndExit } from '../../tools/fp-tools'
import { isNotNullNorUndefined, isNullOrUndefinedOrEmpty } from 'camino-common/src/typescript-tools'
import { getAvisNom } from 'camino-common/src/static/avisTypes'
export type NewDownload = (params: Record<string, unknown>, user: User, pool: Pool) => Promise<{ loid: number | null; fileName: string }>
export const DOWNLOADS_DIRECTORY = 'downloads'
......@@ -37,7 +44,15 @@ export const etapeTelecharger =
)
const entrepriseDocuments = await callAndExit(getEntrepriseDocumentLargeObjectIdsByEtapeId({ titre_etape_id: etapeId }, pool, user))
if (!documents.length && !entrepriseDocuments.length) {
const avisDocuments = await callAndExit(
getEtapeAvisLargeObjectIdsByEtapeId(etapeId, pool, user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires, etapeData.etape_type_id, {
demarche_type_id: etapeData.demarche_type_id,
titre_public_lecture: etapeData.titre_public_lecture,
entreprises_lecture: etapeData.demarche_entreprises_lecture,
public_lecture: etapeData.demarche_public_lecture,
})
)
if (isNullOrUndefinedOrEmpty(documents) && isNullOrUndefinedOrEmpty(entrepriseDocuments) && isNullOrUndefinedOrEmpty(avisDocuments)) {
throw new Error(`aucun document n'a été trouvé pour l'étape "${EtapesTypes[etapeData.etape_type_id].nom}"`)
}
......@@ -64,6 +79,22 @@ export const etapeTelecharger =
const fileName = slugify(`${entrepriseDocument.id}-${DocumentsTypes[entrepriseDocument.entreprise_document_type_id].nom}`)
zip.file(`${fileName}.pdf`, stream)
}
for (let i = 0; i < avisDocuments.length; i++) {
const avisDocument = avisDocuments[i]
if (isNotNullNorUndefined(avisDocument.largeobject_id)) {
await client.query('BEGIN')
const [_size, stream] = await man.openAndReadableStreamAsync(avisDocument.largeobject_id, bufferSize)
let fileName = slugify(`${avisDocument.id}-${avisDocument.date}-${avisDocument.avis_statut_id}`)
// On gère les anciens Ids à l'époque où certains avis étaient des documents
if (!avisDocument.id.startsWith('avis')) {
fileName = slugify(`avis-${getAvisNom(avisDocument.avis_type_id)}-${avisDocument.id}-${avisDocument.avis_statut_id}`)
}
zip.file(`${fileName}.pdf`, stream)
}
}
const nom = `documents-${etapeData.etape_slug}.zip`
const filePath = `/${DOWNLOADS_DIRECTORY}/${nom}`
......
......@@ -621,8 +621,8 @@ const categoriesForTaxeAurifereGuyane = {
reference: 'https://www.legifrance.gouv.fr/codes/id/LEGIARTI000048046958/2023-09-07',
},
'2024': {
value: new Decimal(549.88),
reference: 'https://www.legifrance.gouv.fr/codes/id/LEGIARTI000048046958/2024-09-07',
value: new Decimal(577.19),
reference: 'https://www.legifrance.gouv.fr/codes/id/LEGIARTI000048046958/2024-12-06',
},
},
autre: {
......@@ -655,8 +655,8 @@ const categoriesForTaxeAurifereGuyane = {
reference: 'https://www.legifrance.gouv.fr/codes/id/LEGIARTI000048046958/2023-09-07',
},
'2024': {
value: new Decimal(1099.77),
reference: 'https://www.legifrance.gouv.fr/codes/id/LEGIARTI000048046958/2024-09-07',
value: new Decimal(1154.38),
reference: 'https://www.legifrance.gouv.fr/codes/id/LEGIARTI000048046958/2024-12-06',
},
},
} as const satisfies Record<EntrepriseCategory, Record<AnneeData, { value: Decimal; reference: string }>>
......
......@@ -612,7 +612,7 @@ describe('titreDemarcheUpdatedEtatValidate', () => {
).toMatchInlineSnapshot(`
{
"errors": [
"les étapes de la démarche machine ne sont pas valides",
"les étapes de la démarche machine AXMOct ne sont pas valides",
],
"valid": false,
}
......@@ -662,7 +662,7 @@ describe('titreDemarcheUpdatedEtatValidate', () => {
).toMatchInlineSnapshot(`
{
"errors": [
"les étapes de la démarche machine ne sont pas valides",
"les étapes de la démarche machine AXMOct ne sont pas valides",
],
"valid": false,
}
......
......@@ -79,7 +79,7 @@ export const titreDemarcheUpdatedEtatValidate = (
const statutPossiblesPourCetteEtape = etapeTypesWithStatusPossibles[titreEtape.typeId]
if (isNullOrUndefined(statutPossiblesPourCetteEtape) || !statutPossiblesPourCetteEtape.etapeStatutIds.includes(titreEtape.statutId)) {
if (isNotNullNorUndefined(machine)) {
return { valid: false, errors: ['les étapes de la démarche machine ne sont pas valides'] }
return { valid: false, errors: [`les étapes de la démarche machine ${machine.machine.id} ne sont pas valides`] }
} else {
return { valid: false, errors: ['les étapes de la démarche TDE ne sont pas valides'] }
}
......
......@@ -231,7 +231,7 @@ describe('valide l’étape avant de l’enregistrer', () => {
)
expect(errors).toMatchInlineSnapshot(`
[
"les étapes de la démarche machine ne sont pas valides",
"les étapes de la démarche machine oct ne sont pas valides",
]
`)
})
......
import { IHeritageProps, IHeritageContenu } from '../../../types'
import { userSuper } from '../../user-super'
import { titreEtapeGet } from '../../queries/titres-etapes'
import { newEtapeId } from './id-create'
import { isHeritageProps } from 'camino-common/src/heritage'
import { getKeys } from 'camino-common/src/typescript-tools'
import { getKeys, isNullOrUndefined } from 'camino-common/src/typescript-tools'
import { FieldsEtape } from '../../queries/_options'
export const heritagePropsFormat = async (heritageProps: IHeritageProps): Promise<IHeritageProps> => {
......@@ -11,8 +10,10 @@ export const heritagePropsFormat = async (heritageProps: IHeritageProps): Promis
if (heritageProps[propId].etapeId) {
const fields: FieldsEtape = { id: {} }
const titreEtape = await titreEtapeGet(newEtapeId(heritageProps[propId].etapeId!), { fields }, userSuper)
const titreEtape = await titreEtapeGet(heritageProps[propId].etapeId, { fields }, userSuper)
if (isNullOrUndefined(titreEtape)) {
throw new Error(`Impossible de récupérer l'héritage d'une étape qui n'existe pas ${heritageProps[propId].etapeId}`)
}
heritageProps[propId].etape = titreEtape
}
}
......@@ -26,8 +27,10 @@ export const heritageContenuFormat = async (heritageContenu: IHeritageContenu):
if (heritageContenu[sectionId]) {
for (const elementId of Object.keys(heritageContenu[sectionId])) {
if (heritageContenu[sectionId][elementId]?.etapeId) {
const titreEtape = await titreEtapeGet(newEtapeId(heritageContenu[sectionId][elementId].etapeId!), { fields }, userSuper)
const titreEtape = await titreEtapeGet(heritageContenu[sectionId][elementId].etapeId, { fields }, userSuper)
if (isNullOrUndefined(titreEtape)) {
throw new Error(`Impossible de récupérer le contenu de l'héritage d'une étape qui n'existe pas ${heritageContenu[sectionId][elementId].etapeId}`)
}
heritageContenu[sectionId][elementId].etape = titreEtape
}
}
......
......@@ -380,7 +380,7 @@ where
`
const errorCanReadDocument = "une erreur s'est produite lors de la vérification des droits de lecture d'un document" as const
type GetEtapeDocumentLargeObjectIdsByEtapeIdErrors = EffectDbQueryAndValidateErrors | typeof errorCanReadDocument
export type GetEtapeDocumentLargeObjectIdsByEtapeIdErrors = EffectDbQueryAndValidateErrors | typeof errorCanReadDocument
export const getEtapeDocumentLargeObjectIdsByEtapeId = (
titre_etape_id: EtapeId,
pool: Pool,
......@@ -438,7 +438,7 @@ where
`
const errorCanReadAvis = "une erreur s'est produite lors de la vérification des droits de lecture d'un avis" as const
type GetEtapeAvisLargeObjectIdsByEtapeIdErrors = EffectDbQueryAndValidateErrors | typeof errorCanReadAvis
export type GetEtapeAvisLargeObjectIdsByEtapeIdErrors = EffectDbQueryAndValidateErrors | typeof errorCanReadAvis
export const getEtapeAvisLargeObjectIdsByEtapeId = (
titre_etape_id: EtapeId,
pool: Pool,
......