Skip to content

Commit

Permalink
feat(démarche): ne peut plus créer de nouvelle démarche tant que la p…
Browse files Browse the repository at this point in the history
…remière démarche est en instruction (#1190)
  • Loading branch information
vmaubert committed May 29, 2024
1 parent 118107b commit 8bfdfe7
Show file tree
Hide file tree
Showing 19 changed files with 451 additions and 44 deletions.
11 changes: 7 additions & 4 deletions packages/api/src/api/graphql/resolvers/titres-demarches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import { titreGet } from '../../../database/queries/titres.js'
import { titreDemarcheUpdate as titreDemarcheUpdateTask } from '../../../business/titre-demarche-update.js'
import { titreDemarcheUpdationValidate } from '../../../business/validations/titre-demarche-updation-validate.js'
import { isDemarcheTypeId, isTravaux } from 'camino-common/src/static/demarchesTypes.js'
import { canCreateTravaux, canCreateOrEditDemarche, canDeleteDemarche } from 'camino-common/src/permissions/titres-demarches.js'
import { canCreateTravaux, canEditDemarche, canDeleteDemarche, canCreateDemarche } from 'camino-common/src/permissions/titres-demarches.js'
import { isNullOrUndefined } from 'camino-common/src/typescript-tools.js'
import { userSuper } from '../../../database/user-super.js'
import { getDemarchesByTitreId } from '../../rest/titres.queries.js'

export const demarches = async (
{
Expand Down Expand Up @@ -146,10 +147,12 @@ export const demarcheCreer = async ({ demarche }: { demarche: ITitreDemarche },
throw new Error('le statut du titre est obligatoire')
}

if (isTravaux(demarche.typeId) && !canCreateTravaux(user, titre.typeId, titre.administrationsLocales ?? [])) {
const demarches = await getDemarchesByTitreId(pool, demarche.titreId)

if (isTravaux(demarche.typeId) && !canCreateTravaux(user, titre.typeId, titre.administrationsLocales ?? [], demarches)) {
throw new Error('droits insuffisants')
}
if (!isTravaux(demarche.typeId) && !canCreateOrEditDemarche(user, titre.typeId, titre.titreStatutId, titre.administrationsLocales ?? [])) {
if (!isTravaux(demarche.typeId) && !canCreateDemarche(user, titre.typeId, titre.titreStatutId, titre.administrationsLocales ?? [], demarches)) {
throw new Error('droits insuffisants')
}

Expand Down Expand Up @@ -184,7 +187,7 @@ export const demarcheModifier = async ({ demarche }: { demarche: ITitreDemarche
if (isNullOrUndefined(titre)) throw new Error("le titre n'existe pas")
if (isNullOrUndefined(titre.administrationsLocales)) throw new Error('les administrations locales ne sont pas chargées')

if (!canCreateOrEditDemarche(user, titre.typeId, titre.titreStatutId, titre.administrationsLocales)) throw new Error('droits insuffisants')
if (!canEditDemarche(user, titre.typeId, titre.titreStatutId, titre.administrationsLocales)) throw new Error('droits insuffisants')

if (demarcheOld.titreId !== demarche.titreId) throw new Error('le titre n’existe pas')

Expand Down
5 changes: 4 additions & 1 deletion packages/api/src/api/rest/titres.queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const getTitre = async (pool: Pool, user: User, idOrSlug: TitreIdOrSlug):
const titreDateLastModified = await getDateLastJournal(pool, titre.id)
const titreDoublon = titre.titre_doublon_id !== null && titre.titre_doublon_nom !== null ? { id: titre.titre_doublon_id, nom: titre.titre_doublon_nom } : null

const demarchesFromDatabase = await dbQueryAndValidate(getDemarchesByTitreIdQueryDb, { titreId: titre.id }, pool, getDemarchesByTitreIdQueryDbValidator)
const demarchesFromDatabase = await getDemarchesByTitreId(pool, titre.id)

const titreTypeId = memoize(() => Promise.resolve(titre.titre_type_id))

Expand Down Expand Up @@ -319,6 +319,9 @@ const getDemarchesByTitreIdQueryDbValidator = z.object({
})
type GetDemarchesByTitreIdQueryDb = z.infer<typeof getDemarchesByTitreIdQueryDbValidator>

export const getDemarchesByTitreId = async (pool: Pool, titreId: TitreId) => {
return dbQueryAndValidate(getDemarchesByTitreIdQueryDb, { titreId }, pool, getDemarchesByTitreIdQueryDbValidator)
}
const getDemarchesByTitreIdQueryDb = sql<Redefine<IGetDemarchesByTitreIdQueryDbQuery, { titreId: TitreId }, GetDemarchesByTitreIdQueryDb>>`
select
d.id,
Expand Down
17 changes: 13 additions & 4 deletions packages/api/src/api/rest/titres.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { DemarchesStatutsIds } from 'camino-common/src/static/demarchesStatuts.j
import { ETAPES_TYPES, EtapeTypeId } from 'camino-common/src/static/etapesTypes.js'
import { CaminoDate, getCurrent } from 'camino-common/src/date.js'
import { isAdministration, User } from 'camino-common/src/roles.js'
import { canCreateOrEditDemarche, canCreateTravaux } from 'camino-common/src/permissions/titres-demarches.js'
import { canEditDemarche, canCreateTravaux } from 'camino-common/src/permissions/titres-demarches.js'
import { utilisateurTitreCreate, utilisateurTitreDelete } from '../../database/queries/utilisateurs.js'
import titreUpdateTask from '../../business/titre-update.js'
import { getTitre as getTitreDb } from './titres.queries.js'
Expand Down Expand Up @@ -195,7 +195,7 @@ export const titresAdministrations = (_pool: Pool) => async (req: CaminoRequest,
const titresAutorises = await titresGet(
filters,
{
fields: { pointsEtape: { id: {} } },
fields: { pointsEtape: { id: {} }, demarches: { id: {} } },
},
user
)
Expand All @@ -209,10 +209,19 @@ export const titresAdministrations = (_pool: Pool) => async (req: CaminoRequest,
throw new Error('les administrations locales doivent être chargées')
}

if (titre.demarches === undefined) {
throw new Error('les démarches doivent être chargées')
}

return (
canEditTitre(user, titre.typeId, titre.titreStatutId) ||
canCreateOrEditDemarche(user, titre.typeId, titre.titreStatutId, titre.administrationsLocales ?? []) ||
canCreateTravaux(user, titre.typeId, titre.administrationsLocales ?? [])
canEditDemarche(user, titre.typeId, titre.titreStatutId, titre.administrationsLocales ?? []) ||
canCreateTravaux(
user,
titre.typeId,
titre.administrationsLocales ?? [],
titre.demarches.map(({ demarcheDateDebut }) => ({ demarche_date_debut: demarcheDateDebut ?? null }))
)
)
})
.map(({ id }) => id)
Expand Down
48 changes: 35 additions & 13 deletions packages/common/src/permissions/titres-demarches.test.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
import { describe, expect, test } from 'vitest'
import { AdministrationId } from '../static/administrations.js'
import { canCreateOrEditDemarche, canCreateTravaux, canDeleteDemarche } from './titres-demarches.js'
import { canEditDemarche, canCreateTravaux, canDeleteDemarche, canCreateDemarche } from './titres-demarches.js'
import { testBlankUser, TestUser } from '../tests-utils.js'
import { TitresStatutIds } from '../static/titresStatuts.js'
import { caminoDateValidator } from '../date.js'

describe('canCreateOrEditDemarche', () => {
describe('canEditDemarche', () => {
test.each<[AdministrationId, boolean]>([
['dre-ile-de-france-01', false],
['dea-guadeloupe-01', false],
['min-mtes-dgec-01', false],
['pre-42218-01', false],
['ope-ptmg-973-01', true],
['dea-guyane-01', false],
])('Vérifie si l’administration peut créer des démarches', async (administrationId, creation) => {
expect(canCreateOrEditDemarche({ role: 'admin', administrationId, ...testBlankUser }, 'arm', TitresStatutIds.Valide, [])).toEqual(creation)
])('Vérifie si l’administration peut modifier des démarches', async (administrationId, creation) => {
expect(canEditDemarche({ role: 'admin', administrationId, ...testBlankUser }, 'arm', TitresStatutIds.Valide, [])).toEqual(creation)
})

test('Une administration locale peut créer des démarches', () => {
expect(canCreateOrEditDemarche({ role: 'admin', administrationId: 'dea-guyane-01', ...testBlankUser }, 'axm', TitresStatutIds.Valide, ['dea-guyane-01'])).toEqual(true)
test('Une administration locale peut modifier des démarches', () => {
expect(canEditDemarche({ role: 'admin', administrationId: 'dea-guyane-01', ...testBlankUser }, 'axm', TitresStatutIds.Valide, ['dea-guyane-01'])).toEqual(true)
})

test('Le PTMG ne peut pas créer de démarche sur une ARM classée', () => {
expect(canCreateOrEditDemarche({ role: 'admin', administrationId: 'ope-ptmg-973-01', ...testBlankUser }, 'arm', TitresStatutIds.DemandeClassee, [])).toEqual(false)
test('Le PTMG ne peut pas modifier de démarche sur une ARM classée', () => {
expect(canEditDemarche({ role: 'admin', administrationId: 'ope-ptmg-973-01', ...testBlankUser }, 'arm', TitresStatutIds.DemandeClassee, [])).toEqual(false)
})

test.each<[TestUser, boolean]>([
[{ role: 'super' }, true],
[{ role: 'entreprise', entreprises: [] }, false],
[{ role: 'defaut' }, false],
])('Vérifie si un profil peut créer des démarches', async (user, creation) => {
expect(canCreateOrEditDemarche({ ...user, ...testBlankUser }, 'arm', TitresStatutIds.Valide, [])).toEqual(creation)
])('Vérifie si un profil peut modifier des démarches', async (user, creation) => {
expect(canEditDemarche({ ...user, ...testBlankUser }, 'arm', TitresStatutIds.Valide, [])).toEqual(creation)
})
})

Expand Down Expand Up @@ -62,6 +63,23 @@ describe('canDeleteDemarche', () => {
})
})

describe('canCreateDemarche', () => {
test('Une administration locale peut créer des démarches si il n’y a pas d’octroi en cours de construction', () => {
expect(
canCreateDemarche(
{ role: 'admin', administrationId: 'dea-guyane-01', ...testBlankUser },
'axm',
TitresStatutIds.Valide,
['dea-guyane-01'],
[{ demarche_date_debut: caminoDateValidator.parse('2023-01-01') }]
)
).toEqual(true)
})
test('Une administration locale ne peut pas créer de démarche si il y a un octroi en cours de construction', () => {
expect(canCreateDemarche({ role: 'admin', administrationId: 'dea-guyane-01', ...testBlankUser }, 'axm', TitresStatutIds.Valide, ['dea-guyane-01'], [{ demarche_date_debut: null }])).toEqual(false)
})
})

describe('canCreateTravaux', () => {
test.each<[AdministrationId, boolean]>([
['dre-ile-de-france-01', false],
Expand All @@ -71,18 +89,22 @@ describe('canCreateTravaux', () => {
['ope-ptmg-973-01', false],
['dea-guyane-01', false],
])('Vérifie si l’administration peut créer des travaux', async (administrationId, creation) => {
expect(canCreateTravaux({ role: 'admin', administrationId, ...testBlankUser }, 'arm', [])).toEqual(creation)
expect(canCreateTravaux({ role: 'admin', administrationId, ...testBlankUser }, 'arm', [], [])).toEqual(creation)
})

test('La DGTM peut créer des travaux sur les AXM', () => {
expect(canCreateTravaux({ role: 'admin', administrationId: 'dea-guyane-01', ...testBlankUser }, 'axm', [])).toEqual(true)
expect(canCreateTravaux({ role: 'admin', administrationId: 'dea-guyane-01', ...testBlankUser }, 'axm', [], [{ demarche_date_debut: caminoDateValidator.parse('2023-01-01') }])).toEqual(true)
})

test('La DGTM ne peut pas créer des travaux sur les AXM si l’octroi n’est pas encore valide', () => {
expect(canCreateTravaux({ role: 'admin', administrationId: 'dea-guyane-01', ...testBlankUser }, 'axm', [], [{ demarche_date_debut: null }])).toEqual(false)
})

test.each<[TestUser, boolean]>([
[{ role: 'super' }, true],
[{ role: 'entreprise', entreprises: [] }, false],
[{ role: 'defaut' }, false],
])('Vérifie si un profil peut créer des travaux', async (user, creation) => {
expect(canCreateTravaux({ ...user, ...testBlankUser }, 'arm', [])).toEqual(creation)
expect(canCreateTravaux({ ...user, ...testBlankUser }, 'arm', [], [])).toEqual(creation)
})
})
24 changes: 22 additions & 2 deletions packages/common/src/permissions/titres-demarches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,24 @@ import { AdministrationId, Administrations } from '../static/administrations.js'
import { getEtapesTDE } from '../static/titresTypes_demarchesTypes_etapesTypes/index.js'
import { DemarcheTypeId } from '../static/demarchesTypes.js'
import { canCreateEtape } from './titres-etapes.js'
import { TitreGetDemarche } from '../titres.js'
import { isNullOrUndefined } from '../typescript-tools.js'

export const canCreateOrEditDemarche = (user: User, titreTypeId: TitreTypeId, titreStatutId: TitreStatutId, administrationsLocales: AdministrationId[]): boolean => {
const hasOneDemarcheWithoutPhase = (demarches: Pick<TitreGetDemarche, 'demarche_date_debut'>[]): boolean => {
// Si il y a une seule démarche et qu’elle n’a pas encore créée de phase, alors on ne peut pas créer une deuxième démarche
return demarches.length === 1 && isNullOrUndefined(demarches[0].demarche_date_debut)
}
export const canCreateDemarche = (
user: User,
titreTypeId: TitreTypeId,
titreStatutId: TitreStatutId,
administrationsLocales: AdministrationId[],
demarches: Pick<TitreGetDemarche, 'demarche_date_debut'>[]
): boolean => {
return !hasOneDemarcheWithoutPhase(demarches) && canEditDemarche(user, titreTypeId, titreStatutId, administrationsLocales)
}

export const canEditDemarche = (user: User, titreTypeId: TitreTypeId, titreStatutId: TitreStatutId, administrationsLocales: AdministrationId[]): boolean => {
if (isSuper(user)) {
return true
} else if (isAdministrationAdmin(user) || isAdministrationEditeur(user)) {
Expand Down Expand Up @@ -36,7 +52,11 @@ export const canDeleteDemarche = (user: User, titreTypeId: TitreTypeId, titreSta
return false
}

export const canCreateTravaux = (user: User, titreTypeId: TitreTypeId, administrations: AdministrationId[]): boolean => {
export const canCreateTravaux = (user: User, titreTypeId: TitreTypeId, administrations: AdministrationId[], demarches: Pick<TitreGetDemarche, 'demarche_date_debut'>[]): boolean => {
if (hasOneDemarcheWithoutPhase(demarches)) {
return false
}

if (isSuper(user)) {
return true
} else if (isAdministrationAdmin(user) || isAdministrationEditeur(user)) {
Expand Down
32 changes: 30 additions & 2 deletions packages/ui/src/components/titre.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ const titresTo: TitreLink[] = [{ id: titreIdValidator.parse('id10'), nom: 'Titre
const titresFrom: TitreLink[] = [linkableTitres[0]]

const demarcheSlug = demarcheSlugValidator.parse('slug-demarche-1')
const titre: TitreGet = {
const titre = {
id: titreIdValidator.parse('id-du-titre'),
nom: 'Nom du titre',
slug: titreSlugValidator.parse('slug-du-titre'),
Expand Down Expand Up @@ -225,7 +225,7 @@ const titre: TitreGet = {
],
},
],
}
} as const satisfies TitreGet

const apiClient: PropsApiClient = {
editTitre: (...params) => {
Expand Down Expand Up @@ -701,3 +701,31 @@ export const Lenoncourt: StoryFn = () => (
titreIdOrSlug={titreIdValidator.parse('s7RvqvCAgKs4DxkQBYV93cVx')}
/>
)

export const TitreAvecUneSeuleDemarcheEnConstruction: StoryFn = () => (
<PureTitre
entreprises={entreprises}
currentDate={currentDate}
currentDemarcheSlug={demarcheSlug}
initTab="points"
user={{ ...testBlankUser, role: 'super' }}
router={routerPushMock}
apiClient={{
...apiClient,
getTitreById: (titreIdOrSlug: TitreIdOrSlug) => {
getTitreAction(titreIdOrSlug)

return Promise.resolve({
...titre,
demarches: [
{
...titre.demarches[0],
demarche_date_debut: null,
},
],
})
},
}}
titreIdOrSlug={titre.id}
/>
)
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ <h2>Phases</h2>
<div>
<div class="fr-grid-row fr-grid-row--middle">
<h2 style="margin: 0px;">Octroi</h2>
<p style="z-index: unset; margin-bottom: 0px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="fr-badge fr-badge--md fr-ml-2w fr-badge--green-bourgeon fr-ml-2w" title="accepté" aria-label="accepté">accepté</p>
<p style="z-index: unset; margin-bottom: 0px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; margin-right: auto;" class="fr-badge fr-badge--md fr-ml-2w fr-badge--green-bourgeon fr-ml-2w" title="accepté" aria-label="accepté">accepté</p>
<!---->
<!---->
<!---->
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ <h2>Phases</h2>
<div>
<div class="fr-grid-row fr-grid-row--middle">
<h2 style="margin: 0px;">Prolongation 1</h2>
<p style="z-index: unset; margin-bottom: 0px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="fr-badge fr-badge--md fr-ml-2w fr-badge--green-bourgeon fr-ml-2w" title="accepté" aria-label="accepté">accepté</p>
<p style="z-index: unset; margin-bottom: 0px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; margin-right: auto;" class="fr-badge fr-badge--md fr-ml-2w fr-badge--green-bourgeon fr-ml-2w" title="accepté" aria-label="accepté">accepté</p>
<!---->
<!---->
<!---->
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ <h2>Phases</h2>
<div>
<div class="fr-grid-row fr-grid-row--middle">
<h2 style="margin: 0px;">Octroi</h2>
<p style="z-index: unset; margin-bottom: 0px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="fr-badge fr-badge--md fr-ml-2w fr-badge--green-bourgeon fr-ml-2w" title="accepté" aria-label="accepté">accepté</p>
<p style="z-index: unset; margin-bottom: 0px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; margin-right: auto;" class="fr-badge fr-badge--md fr-ml-2w fr-badge--green-bourgeon fr-ml-2w" title="accepté" aria-label="accepté">accepté</p>
<!---->
<!---->
<!---->
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ <h2>Phases</h2>
<div>
<div class="fr-grid-row fr-grid-row--middle">
<h2 style="margin: 0px;">Prolongation 2</h2>
<p style="z-index: unset; margin-bottom: 0px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="fr-badge fr-badge--md fr-ml-2w fr-badge--green-bourgeon fr-ml-2w" title="accepté" aria-label="accepté">accepté</p>
<p style="z-index: unset; margin-bottom: 0px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; margin-right: auto;" class="fr-badge fr-badge--md fr-ml-2w fr-badge--green-bourgeon fr-ml-2w" title="accepté" aria-label="accepté">accepté</p>
<!---->
<!---->
<!---->
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ <h2>Phases</h2>
<div>
<div class="fr-grid-row fr-grid-row--middle">
<h2 style="margin: 0px;">Déclaration d'arrêt définitif des travaux</h2>
<p style="z-index: unset; margin-bottom: 0px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="fr-badge fr-badge--md fr-ml-2w fr-badge--green-bourgeon fr-ml-2w" title="fin de la police des mines" aria-label="fin de la police des mines">fin de la police des mines</p>
<p style="z-index: unset; margin-bottom: 0px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; margin-right: auto;" class="fr-badge fr-badge--md fr-ml-2w fr-badge--green-bourgeon fr-ml-2w" title="fin de la police des mines" aria-label="fin de la police des mines">fin de la police des mines</p>
<!---->
<!---->
<!---->
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ <h2>Phases</h2>
<div>
<div class="fr-grid-row fr-grid-row--middle">
<h2 style="margin: 0px;">Mutation</h2>
<p style="z-index: unset; margin-bottom: 0px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="fr-badge fr-badge--md fr-ml-2w fr-badge--green-bourgeon fr-ml-2w" title="accepté" aria-label="accepté">accepté</p><button class="fr-btn fr-btn--primary fr-btn--md" title="Ajouter une démarche" aria-label="Ajouter une démarche" type="button" style="margin-left: auto;">Ajouter une démarche</button><button class="fr-btn fr-btn--secondary fr-btn--md fr-icon-pencil-line fr-ml-2w" title="Modifier la description" aria-label="Modifier la description" type="button" style="margin-right: 0px;">
<p style="z-index: unset; margin-bottom: 0px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; margin-right: auto;" class="fr-badge fr-badge--md fr-ml-2w fr-badge--green-bourgeon fr-ml-2w" title="accepté" aria-label="accepté">accepté</p><button class="fr-btn fr-btn--primary fr-btn--md" title="Ajouter une démarche" aria-label="Ajouter une démarche" type="button">Ajouter une démarche</button><button class="fr-btn fr-btn--secondary fr-btn--md fr-icon-pencil-line fr-ml-2w" title="Modifier la description" aria-label="Modifier la description" type="button" style="margin-right: 0px;">
<!---->
</button><button class="fr-btn fr-btn--secondary fr-btn--md fr-icon-delete-bin-line fr-ml-2w" title="Supprimer la démarche" aria-label="Supprimer la démarche" type="button">
<!---->
Expand Down
Loading

0 comments on commit 8bfdfe7

Please sign in to comment.